From 716632bc49a5f2a72e5726af3a4267c300bbd6ec Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Fri, 11 Nov 2022 09:33:36 -0800 Subject: [PATCH 01/32] Add support for end_session_uri --- lib/openid_connect.ex | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/openid_connect.ex b/lib/openid_connect.ex index a532724..53e7d62 100644 --- a/lib/openid_connect.ex +++ b/lib/openid_connect.ex @@ -105,6 +105,38 @@ defmodule OpenIDConnect do build_uri(uri, params) end + @spec end_session_uri(provider, params, name) :: uri + @doc """ + Builds the end session URI according to the speci in the providers discovery document + + The `params` option can be used to add additional query params to the URI + + Example: + OpenIDConnect.end_session_uri(:azure, %{"client_id" => "5d4c39b4-660f-41c9-9a99-2a6a9c263f07"}) + + See more about this feature of the OpenID Connect spec: + https://openid.net/specs/openid-connect-rpinitiated-1_0.html + + Each provider will typically require one or more of the supported query params, e.g. `id_token_hint` or + `client_id`. Read your provider's OIDC documentation to determine which one(s) you should add. + """ + def end_session_uri(provider, params \\ %{}, name \\ :openid_connect) do + document = discovery_document(provider, name) + config = config(provider, name) + + uri = Map.get(document, "end_session_endpoint") + + params = + Map.merge( + %{ + client_id: client_id(config) + }, + params + ) + + build_uri(uri, params) + end + @spec fetch_tokens(provider, params, name) :: success(map) | error(:fetch_tokens) @doc """ Fetches the authentication tokens from the provider From 81d8bedee8846db479b51ab9f7b537870df96a86 Mon Sep 17 00:00:00 2001 From: Jamil Date: Fri, 11 Nov 2022 14:53:42 -0800 Subject: [PATCH 02/32] Update lib/openid_connect.ex --- lib/openid_connect.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openid_connect.ex b/lib/openid_connect.ex index 53e7d62..aa6fe59 100644 --- a/lib/openid_connect.ex +++ b/lib/openid_connect.ex @@ -107,7 +107,7 @@ defmodule OpenIDConnect do @spec end_session_uri(provider, params, name) :: uri @doc """ - Builds the end session URI according to the speci in the providers discovery document + Builds the end session URI according to the spec in the providers discovery document The `params` option can be used to add additional query params to the URI From c45731401dcadf387eddd540db9cf2c7187d83e5 Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Sat, 12 Nov 2022 12:15:27 -0800 Subject: [PATCH 03/32] Add more examples of provider fixtures; update deps --- config/config.exs | 2 +- config/dev.exs | 2 +- config/test.exs | 2 +- mix.exs | 2 +- mix.lock | 38 +++++++------ test/fixtures/google/certs.exs | 41 ------------- test/fixtures/google/discovery_document.exs | 57 ------------------- .../http/auth0/discovery_document.exs | 43 ++++++++++++++ test/fixtures/http/auth0/jwks.exs | 43 ++++++++++++++ .../http/azure/discovery_document.exs | 35 ++++++++++++ test/fixtures/http/azure/jwks.exs | 35 ++++++++++++ .../http/google/discovery_document.exs | 36 ++++++++++++ test/fixtures/http/google/jwks.exs | 29 ++++++++++ .../http/keycloak/discovery_document.exs | 23 ++++++++ test/fixtures/http/keycloak/jwks.exs | 23 ++++++++ .../fixtures/http/okta/discovery_document.exs | 34 +++++++++++ test/fixtures/http/okta/jwks.exs | 36 ++++++++++++ .../http/onelogin/discovery_document.exs | 25 ++++++++ test/fixtures/http/onelogin/jwks.exs | 25 ++++++++ .../http/vault/discovery_document.exs | 20 +++++++ test/fixtures/http/vault/jwks.exs | 20 +++++++ test/fixtures/{rsa => jwks}/jwk1.exs | 0 test/fixtures/{rsa => jwks}/jwk2.exs | 0 test/fixtures/{rsa => jwks}/jwks.exs | 0 test/openid_connect/worker_test.exs | 2 +- test/openid_connect_test.exs | 30 +++++----- test/support/fixtures.ex | 15 ++--- test/support/mock_worker.ex | 2 +- 28 files changed, 476 insertions(+), 144 deletions(-) delete mode 100644 test/fixtures/google/certs.exs delete mode 100644 test/fixtures/google/discovery_document.exs create mode 100644 test/fixtures/http/auth0/discovery_document.exs create mode 100644 test/fixtures/http/auth0/jwks.exs create mode 100644 test/fixtures/http/azure/discovery_document.exs create mode 100644 test/fixtures/http/azure/jwks.exs create mode 100644 test/fixtures/http/google/discovery_document.exs create mode 100644 test/fixtures/http/google/jwks.exs create mode 100644 test/fixtures/http/keycloak/discovery_document.exs create mode 100644 test/fixtures/http/keycloak/jwks.exs create mode 100644 test/fixtures/http/okta/discovery_document.exs create mode 100644 test/fixtures/http/okta/jwks.exs create mode 100644 test/fixtures/http/onelogin/discovery_document.exs create mode 100644 test/fixtures/http/onelogin/jwks.exs create mode 100644 test/fixtures/http/vault/discovery_document.exs create mode 100644 test/fixtures/http/vault/jwks.exs rename test/fixtures/{rsa => jwks}/jwk1.exs (100%) rename test/fixtures/{rsa => jwks}/jwk2.exs (100%) rename test/fixtures/{rsa => jwks}/jwks.exs (100%) diff --git a/config/config.exs b/config/config.exs index f321714..23f98d3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. -use Mix.Config +import Config # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this diff --git a/config/dev.exs b/config/dev.exs index 18c4b68..939bb2d 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :openid_connect, :providers, google: [ diff --git a/config/test.exs b/config/test.exs index e4dfa14..e6448bb 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :openid_connect, :http_client, OpenIDConnect.HTTPClientMock diff --git a/mix.exs b/mix.exs index 3c3f5e6..153e8fd 100644 --- a/mix.exs +++ b/mix.exs @@ -62,7 +62,7 @@ defmodule OpenIDConnect.Mixfile do {:jose, "~> 1.8"}, {:earmark, "~> 1.2", only: :dev}, {:ex_doc, "~> 0.18", only: :dev}, - {:mox, "~> 0.4", only: :test} + {:mox, "~> 1.0", only: :test} ] end end diff --git a/mix.lock b/mix.lock index 9d5194b..6a81efc 100644 --- a/mix.lock +++ b/mix.lock @@ -1,22 +1,24 @@ %{ - "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, - "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "cutkey": {:git, "https://github.com/potatosalad/cutkey.git", "47640d04fb4db4a0b79168d7fca0df87aaa42751", []}, - "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, - "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"}, - "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"}, - "httpoison": {:hex, :httpoison, "1.4.0", "e0b3c2ad6fa573134e42194d13e925acfa8f89d138bc621ffb7b1989e6d22e73", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, - "mox": {:hex, :mox, "0.4.0", "7f120840f7d626184a3d65de36189ca6f37d432e5d63acd80045198e4c5f7e6e", [:mix], [], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.4.33", "2b33a505180583f98bfa17317f03973b52081bdb24a11be05a7f4fa6d64dd8bf", [:mix], [{:earmark_parser, "~> 1.4.29", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "21b31363d6a0a70802cfbaf2de88355778aa76654298a072bce2e01d1858ae06"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, + "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/fixtures/google/certs.exs b/test/fixtures/google/certs.exs deleted file mode 100644 index 46cf037..0000000 --- a/test/fixtures/google/certs.exs +++ /dev/null @@ -1,41 +0,0 @@ -%HTTPoison.Response{ - body: %{ - "keys" => [ - %{ - "alg" => "RS256", - "e" => "AQAB", - "kid" => "dad44739576485ec30d228842e73ace0bc367bc4", - "kty" => "RSA", - "n" => - "0_8faqWY4x8LCariP6tIbqt2atUM2nSKAuTb57NYZYOGeUYo4uaEhJjF6sJ2djOSdpj-a70zEamhlHGyCsgzNroQX70oaL05zL1wV0Q9TPHa1qRobVDSBglhmJLHfER9L0mk9jokXJhWpL7NFU7qyKCD7gwe84NR4C7emXtfQGzFS4VcKqxNO17uwmRKzx_ZMy4St999KEDBPCtuq8XGvjMYZE2Rfk6-LWcmAtJ7COOS1yfEfAgbvCAbDKnIZqojPusn_5jRUOotJd2T3GgnSMEFn1G1DGK7hNHLBKqzimeEMTQpnhYXbPwqTbAAll3rtKV4PCM585QTsV0U3U5VaQ", - "use" => "sig" - }, - %{ - "alg" => "RS256", - "e" => "AQAB", - "kid" => "4ef5118b0800bd60a4194186dcb538fc66e5eb34", - "kty" => "RSA", - "n" => - "4ZSPB8TO7y3xZF_GxB_JSx_yBEtNs0mDilLvesSLLypYmxt4U7Dxk-vLAf1IVRwaZeeqQRIhrKJjljIqd33tVwfAp5PinjUm7lHi-ufZ_VNQw3uJA5_3tmkMWaLcvdRcILFMlVfBcESp-R5mcF6-bMeYH0n3D5CCJKspIqDERD1gQxfVxWDzafyrqkIROXKEtv3rMe7Z9Yc4mBsL02G6dDKVbjSxvkZ14wMykXEnGkfIiTUSiH8Qm1rdniZigPv2Pa2uSnJ94V-tIDHigjkXR7Cfun4Z38KZdSDRNgJr-m41Pu-plX98j59iGvVyaKP24ZbukGIJRPHYn06xkQeoWw", - "use" => "sig" - } - ] - }, - headers: [ - {"Expires", "Sun, 06 May 2018 03:43:54 GMT"}, - {"Date", "Sat, 05 May 2018 21:58:32 GMT"}, - {"Vary", "Origin"}, - {"Vary", "X-Origin"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"X-Content-Type-Options", "nosniff"}, - {"X-Frame-Options", "SAMEORIGIN"}, - {"X-XSS-Protection", "1; mode=block"}, - {"Content-Length", "987"}, - {"Server", "GSE"}, - {"Cache-Control", "public, max-age=20722, must-revalidate, no-transform"}, - {"Age", "3972"}, - {"Alt-Svc", - "hq=\":443\"; ma=2592000; quic=51303433; quic=51303432; quic=51303431; quic=51303339; quic=51303335,quic=\":443\"; ma=2592000; v=\"43,42,41,39,35\""} - ], - status_code: 200 -} diff --git a/test/fixtures/google/discovery_document.exs b/test/fixtures/google/discovery_document.exs deleted file mode 100644 index 08f1417..0000000 --- a/test/fixtures/google/discovery_document.exs +++ /dev/null @@ -1,57 +0,0 @@ -%HTTPoison.Response{ - body: %{ - "authorization_endpoint" => "https://accounts.google.com/o/oauth2/v2/auth", - "claims_supported" => [ - "aud", - "email", - "email_verified", - "exp", - "family_name", - "given_name", - "iat", - "iss", - "locale", - "name", - "picture", - "sub" - ], - "code_challenge_methods_supported" => ["plain", "S256"], - "id_token_signing_alg_values_supported" => ["RS256"], - "issuer" => "https://accounts.google.com", - "jwks_uri" => "https://www.googleapis.com/oauth2/v3/certs", - "response_types_supported" => [ - "code", - "token", - "id_token", - "code token", - "code id_token", - "token id_token", - "code token id_token", - "none" - ], - "revocation_endpoint" => "https://accounts.google.com/o/oauth2/revoke", - "scopes_supported" => ["openid", "email", "profile"], - "subject_types_supported" => ["public"], - "token_endpoint" => "https://www.googleapis.com/oauth2/v4/token", - "token_endpoint_auth_methods_supported" => ["client_secret_post", "client_secret_basic"], - "userinfo_endpoint" => "https://www.googleapis.com/oauth2/v3/userinfo" - }, - headers: [ - {"Accept-Ranges", "none"}, - {"Vary", "Accept-Encoding"}, - {"Content-Type", "application/json"}, - {"Access-Control-Allow-Origin", "*"}, - {"Date", "Mon, 30 Apr 2018 06:25:58 GMT"}, - {"Expires", "Mon, 30 Apr 2018 07:25:58 GMT"}, - {"Last-Modified", "Mon, 01 Feb 2016 19:53:44 GMT"}, - {"X-Content-Type-Options", "nosniff"}, - {"Server", "sffe"}, - {"X-XSS-Protection", "1; mode=block"}, - {"Age", "700"}, - {"Cache-Control", "public, max-age=3600"}, - {"Alt-Svc", - "hq=\":443\"; ma=2592000; quic=51303433; quic=51303432; quic=51303431; quic=51303339; quic=51303335,quic=\":443\"; ma=2592000; v=\"43,42,41,39,35\""}, - {"Transfer-Encoding", "chunked"} - ], - status_code: 200 -} diff --git a/test/fixtures/http/auth0/discovery_document.exs b/test/fixtures/http/auth0/discovery_document.exs new file mode 100644 index 0000000..c22ff7c --- /dev/null +++ b/test/fixtures/http/auth0/discovery_document.exs @@ -0,0 +1,43 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"issuer\":\"https://common.auth0.com/\",\"authorization_endpoint\":\"https://common.auth0.com/authorize\",\"token_endpoint\":\"https://common.auth0.com/oauth/token\",\"device_authorization_endpoint\":\"https://common.auth0.com/oauth/device/code\",\"userinfo_endpoint\":\"https://common.auth0.com/userinfo\",\"mfa_challenge_endpoint\":\"https://common.auth0.com/mfa/challenge\",\"jwks_uri\":\"https://common.auth0.com/.well-known/jwks.json\",\"registration_endpoint\":\"https://common.auth0.com/oidc/register\",\"revocation_endpoint\":\"https://common.auth0.com/oauth/revoke\",\"scopes_supported\":[\"openid\",\"profile\",\"offline_access\",\"name\",\"given_name\",\"family_name\",\"nickname\",\"email\",\"email_verified\",\"picture\",\"created_at\",\"identities\",\"phone\",\"address\"],\"response_types_supported\":[\"code\",\"token\",\"id_token\",\"code token\",\"code id_token\",\"token id_token\",\"code token id_token\"],\"code_challenge_methods_supported\":[\"S256\",\"plain\"],\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\"],\"subject_types_supported\":[\"public\"],\"id_token_signing_alg_values_supported\":[\"HS256\",\"RS256\"],\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"],\"claims_supported\":[\"aud\",\"auth_time\",\"created_at\",\"email\",\"email_verified\",\"exp\",\"family_name\",\"given_name\",\"iat\",\"identities\",\"iss\",\"name\",\"nickname\",\"phone_number\",\"picture\",\"sub\"],\"request_uri_parameter_supported\":false,\"request_parameter_supported\":false}", + headers: [ + {"Date", "Sat, 12 Nov 2022 19:57:59 GMT"}, + {"Content-Type", "application/json; charset=utf-8"}, + {"Transfer-Encoding", "chunked"}, + {"Connection", "keep-alive"}, + {"CF-Ray", "7691d6fb8d50f96b-SJC"}, + {"Access-Control-Allow-Origin", "*"}, + {"Cache-Control", + "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"}, + {"Last-Modified", "Sat, 12 Nov 2022 19:57:59 GMT"}, + {"Strict-Transport-Security", "max-age=31536000"}, + {"Vary", "Accept-Encoding, Origin, Accept-Encoding"}, + {"CF-Cache-Status", "MISS"}, + {"Access-Control-Allow-Credentials", "false"}, + {"Access-Control-Expose-Headers", + "X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset"}, + {"ot-baggage-auth0-request-id", "7691d6fb8d50f96b"}, + {"ot-tracer-sampled", "true"}, + {"ot-tracer-spanid", "273c5b5354590f5b"}, + {"ot-tracer-traceid", "5026231a2155221a"}, + {"traceparent", "00-00000000000000005026231a2155221a-273c5b5354590f5b-01"}, + {"tracestate", "auth0-request-id=7691d6fb8d50f96b,auth0=true"}, + {"X-Auth0-RequestId", "04426ddefbd03b8009aa"}, + {"X-Content-Type-Options", "nosniff"}, + {"X-RateLimit-Limit", "300"}, + {"X-RateLimit-Remaining", "299"}, + {"X-RateLimit-Reset", "1668283080"}, + {"Server", "cloudflare"}, + {"alt-svc", "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400"} + ], + request_url: "https://common.auth0.com/.well-known/openid-configuration", + request: %HTTPoison.Request{ + method: :get, + url: "https://common.auth0.com/.well-known/openid-configuration", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/auth0/jwks.exs b/test/fixtures/http/auth0/jwks.exs new file mode 100644 index 0000000..ec2f2d1 --- /dev/null +++ b/test/fixtures/http/auth0/jwks.exs @@ -0,0 +1,43 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"keys\":[{\"alg\":\"RS256\",\"kty\":\"RSA\",\"use\":\"sig\",\"n\":\"u4qDTgepr5ifzZOxampfmt9naaih6CgOGGdc3F4Bd9r-R4X4gkIu6S9kN76mZ3O9_q4o4G29yJrEr2bUhYl96vrK-hbFddMhTtxy6nkKqmuPj5PXV5IEBogZwzZebXqARjmOVqxhAgZPLGCoD_ebIzYTT7ozprwlbaBS7ZmRXj3vwlffRae4T-gamIcQWhGLujxOY6FsLUP4ExqAeUcftRd0pi0XGBVL1qmu4xRPLznV6Z7OVHemtgBQ4tAKvwSYQWIDKJ886Y3jhLyQukC10N0clnPVuHDPByGXDPTDvhQRN2xTU4PNjGGu_WDElzenA-8CyZf8gEJ0ZM8_j0kmkQ\",\"e\":\"AQAB\",\"kid\":\"QjYxQ0U1QTVCMzk4RTJFNEIxMERGN0NBOUVBQTk1NDEzRTcwNTVBMg\",\"x5t\":\"QjYxQ0U1QTVCMzk4RTJFNEIxMERGN0NBOUVBQTk1NDEzRTcwNTVBMg\",\"x5c\":[\"MIIC6DCCAdCgAwIBAgIJZ9K1arBHwKy5MA0GCSqGSIb3DQEBBQUAMBsxGTAXBgNVBAMTEGNvbW1vbi5hdXRoMC5jb20wHhcNMTUxMTEwMTMxNjMzWhcNMjkwNzE5MTMxNjMzWjAbMRkwFwYDVQQDExBjb21tb24uYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4qDTgepr5ifzZOxampfmt9naaih6CgOGGdc3F4Bd9r+R4X4gkIu6S9kN76mZ3O9/q4o4G29yJrEr2bUhYl96vrK+hbFddMhTtxy6nkKqmuPj5PXV5IEBogZwzZebXqARjmOVqxhAgZPLGCoD/ebIzYTT7ozprwlbaBS7ZmRXj3vwlffRae4T+gamIcQWhGLujxOY6FsLUP4ExqAeUcftRd0pi0XGBVL1qmu4xRPLznV6Z7OVHemtgBQ4tAKvwSYQWIDKJ886Y3jhLyQukC10N0clnPVuHDPByGXDPTDvhQRN2xTU4PNjGGu/WDElzenA+8CyZf8gEJ0ZM8/j0kmkQIDAQABoy8wLTAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRkWtTfBgFGEkhPxk2TeGcjty6aDTANBgkqhkiG9w0BAQUFAAOCAQEAIFbLdkw6cSXm0OQ/rzBlzM6uBWXAYXQM2HSiMQqW4b/C37LeW0upG/0FU6hWa5fbMQvAf9QSgJsG/7CrQgnjssnqvIlw5S1enj5wzFQd9yHgb/1X+e8qOT7VEXdOZXCR/8aYuLiaHuYyNQ8XMXAl/pwaJCrcq9YhA2kBjJ/rojHXdXd9DbT/TJMXNAaNNMicRN1R8OOw+JBbjxA3dMZ9qKRN89JdTyF6c2G/FTXVw4cBoJi/lVKKBXOwT4DYZtDCyOrFaFyhkop1uRzC74pxp2lRYuaKD8ev6suRJTbukcjeerCtN/AUVXd515FyL1mdS4qVJMnT8lt6NRxXqo3EaQ==\"]},{\"alg\":\"RS256\",\"kty\":\"RSA\",\"use\":\"sig\",\"n\":\"0MdUr5IDFWWPbLFM7itct-DHoAgfAD4gfCUlGlupRIk8AvDBf0allrSjznh78gn0XcCmq5MSKH6dJYAalhz5pnKESCZS0NGRijYCnP0FWdwZmPoRfMRe-O7Fw4ogMYYcWN7Iy_6HBefNvkQ91PhDksm0KpXH-yxrFC8gcbQNV14Q9bRwFq3nWmmDj5A7OvEuFmszIvZtpFbSyrCuqMHYR-Pyo-dWRUJ8egqjiOyn2LLWX0iMASJyasG6wF6cQC7k6sIYacxn_rBtuldKBBRlGboTkFl0NW9_xw4CwGxzFj_-GacCkaL7axObS34UN2bowZkXZsSE5l8v54NjalXHcw\",\"e\":\"AQAB\",\"kid\":\"Lk-5NnjByFf99cg0nxMdi\",\"x5t\":\"WaiAZeN6U1Tc9bpQHGclV01odiw\",\"x5c\":[\"MIIC+zCCAeOgAwIBAgIJKlrwnBd5HfDrMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMTEGNvbW1vbi5hdXRoMC5jb20wHhcNMjAwMzExMTg1NjM1WhcNMzMxMTE4MTg1NjM1WjAbMRkwFwYDVQQDExBjb21tb24uYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0MdUr5IDFWWPbLFM7itct+DHoAgfAD4gfCUlGlupRIk8AvDBf0allrSjznh78gn0XcCmq5MSKH6dJYAalhz5pnKESCZS0NGRijYCnP0FWdwZmPoRfMRe+O7Fw4ogMYYcWN7Iy/6HBefNvkQ91PhDksm0KpXH+yxrFC8gcbQNV14Q9bRwFq3nWmmDj5A7OvEuFmszIvZtpFbSyrCuqMHYR+Pyo+dWRUJ8egqjiOyn2LLWX0iMASJyasG6wF6cQC7k6sIYacxn/rBtuldKBBRlGboTkFl0NW9/xw4CwGxzFj/+GacCkaL7axObS34UN2bowZkXZsSE5l8v54NjalXHcwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSR1Dfrm7GcV0/c0Nbd1q950Kuz1DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAJm/bBoE60mpzzdPGrRvNdZgYEe1faPpj5AIk8h2c+Q5v1lB7olJ4Sfb4WIvFeDxQrxB0GlW8tgN2rgtABUjs5Nhc0h7sfB9MZlkQVAZjTqVFXpv1UzzwI2e0RmD4O6m6CvVE+p5bPHao5XvtgEZKL7Q6VXTLGeyQ9XTnlda2IdcZb4IGQTzrqmq6zlAGn3u4P9hlg+7GlldfC+ROKCreFkJODU+ZPr1c1m0wdZ0UDSvk71PPhlNBLP32jRUlaiL3H9ssMoW9rP0UhYAnP6hY0aC1cmeikF1GQXn4ySK5NlKRV+iA8LY/XhYrqXsSQJPn0RkSj4JRbJbP03SZWh9aOI=\"]}]}", + headers: [ + {"Date", "Sat, 12 Nov 2022 19:58:26 GMT"}, + {"Content-Type", "application/json; charset=utf-8"}, + {"Transfer-Encoding", "chunked"}, + {"Connection", "keep-alive"}, + {"CF-Ray", "7691d7a5d88df96b-SJC"}, + {"Access-Control-Allow-Origin", "*"}, + {"Cache-Control", + "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"}, + {"Last-Modified", "Sat, 12 Nov 2022 19:58:26 GMT"}, + {"Strict-Transport-Security", "max-age=31536000"}, + {"Vary", "Accept-Encoding, Origin, Accept-Encoding"}, + {"CF-Cache-Status", "MISS"}, + {"Access-Control-Allow-Credentials", "false"}, + {"Access-Control-Expose-Headers", + "X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset"}, + {"ot-baggage-auth0-request-id", "7691d7a5d88df96b"}, + {"ot-tracer-sampled", "true"}, + {"ot-tracer-spanid", "7656a9c95f8d129c"}, + {"ot-tracer-traceid", "0d36337a675d4ae3"}, + {"traceparent", "00-00000000000000000d36337a675d4ae3-7656a9c95f8d129c-01"}, + {"tracestate", "auth0-request-id=7691d7a5d88df96b,auth0=true"}, + {"X-Auth0-RequestId", "f7a94a46ab5d86bb7f12"}, + {"X-Content-Type-Options", "nosniff"}, + {"X-RateLimit-Limit", "20"}, + {"X-RateLimit-Remaining", "19"}, + {"X-RateLimit-Reset", "1668283107"}, + {"Server", "cloudflare"}, + {"alt-svc", "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400"} + ], + request_url: "https://common.auth0.com/.well-known/jwks.json", + request: %HTTPoison.Request{ + method: :get, + url: "https://common.auth0.com/.well-known/jwks.json", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/azure/discovery_document.exs b/test/fixtures/http/azure/discovery_document.exs new file mode 100644 index 0000000..884b532 --- /dev/null +++ b/test/fixtures/http/azure/discovery_document.exs @@ -0,0 +1,35 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"token_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/token\",\"token_endpoint_auth_methods_supported\":[\"client_secret_post\",\"private_key_jwt\",\"client_secret_basic\"],\"jwks_uri\":\"https://login.microsoftonline.com/common/discovery/v2.0/keys\",\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\"],\"subject_types_supported\":[\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"response_types_supported\":[\"code\",\"id_token\",\"code id_token\",\"id_token token\"],\"scopes_supported\":[\"openid\",\"profile\",\"email\",\"offline_access\"],\"issuer\":\"https://login.microsoftonline.com/{tenantid}/v2.0\",\"request_uri_parameter_supported\":false,\"userinfo_endpoint\":\"https://graph.microsoft.com/oidc/userinfo\",\"authorization_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\",\"device_authorization_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/devicecode\",\"http_logout_supported\":true,\"frontchannel_logout_supported\":true,\"end_session_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/logout\",\"claims_supported\":[\"sub\",\"iss\",\"cloud_instance_name\",\"cloud_instance_host_name\",\"cloud_graph_host_name\",\"msgraph_host\",\"aud\",\"exp\",\"iat\",\"auth_time\",\"acr\",\"nonce\",\"preferred_username\",\"name\",\"tid\",\"ver\",\"at_hash\",\"c_hash\",\"email\"],\"kerberos_endpoint\":\"https://login.microsoftonline.com/common/kerberos\",\"tenant_region_scope\":null,\"cloud_instance_name\":\"microsoftonline.com\",\"cloud_graph_host_name\":\"graph.windows.net\",\"msgraph_host\":\"graph.microsoft.com\",\"rbac_url\":\"https://pas.windows.net\"}", + headers: [ + {"Cache-Control", "max-age=86400, private"}, + {"Content-Type", "application/json; charset=utf-8"}, + {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, + {"X-Content-Type-Options", "nosniff"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "GET, OPTIONS"}, + {"P3P", "CP=\"DSP CUR OTPi IND OTRi ONL FIN\""}, + {"x-ms-request-id", "d81e7f56-0451-4de4-a5c5-4af112d02001"}, + {"x-ms-ests-server", "2.1.14006.10 - NCUS ProdSlices"}, + {"X-XSS-Protection", "0"}, + {"Set-Cookie", + "fpc=AuKLSwY1b3xLiInKP16p3E4; expires=Mon, 12-Dec-2022 19:36:30 GMT; path=/; secure; HttpOnly; SameSite=None"}, + {"Set-Cookie", + "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7Wevr2ALKzZMjPY-Tt7ffB-f_7y4AMTUR-4m-AQDAi0jJ1K4_N7dY0CZmKZdSweQPMgerZ-TeKnty43nfmYRZS2G39bKUZp5erQLwiB9rkuLis4_ee_cAZK7nh1pkqOh0_t52P9svf75Le0-ex8iyPVhexTbIROTaaYvo6Fl9DFqOtZOnmQplc6ken-ddUcLbnZRSKOTFdr03VB8oSt5gD2BBw2e5qeBuocgX0hS-W-FNbG0gAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None"}, + {"Set-Cookie", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly"}, + {"Set-Cookie", + "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"}, + {"Date", "Sat, 12 Nov 2022 19:36:29 GMT"}, + {"Content-Length", "1547"} + ], + request_url: "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", + request: %HTTPoison.Request{ + method: :get, + url: "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/azure/jwks.exs b/test/fixtures/http/azure/jwks.exs new file mode 100644 index 0000000..18db2af --- /dev/null +++ b/test/fixtures/http/azure/jwks.exs @@ -0,0 +1,35 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"keys\":[{\"kty\":\"RSA\",\"use\":\"sig\",\"kid\":\"nOo3ZDrODXEK1jKWhXslHR_KXEg\",\"x5t\":\"nOo3ZDrODXEK1jKWhXslHR_KXEg\",\"n\":\"oaLLT9hkcSj2tGfZsjbu7Xz1Krs0qEicXPmEsJKOBQHauZ_kRM1HdEkgOJbUznUspE6xOuOSXjlzErqBxXAu4SCvcvVOCYG2v9G3-uIrLF5dstD0sYHBo1VomtKxzF90Vslrkn6rNQgUGIWgvuQTxm1uRklYFPEcTIRw0LnYknzJ06GC9ljKR617wABVrZNkBuDgQKj37qcyxoaxIGdxEcmVFZXJyrxDgdXh9owRmZn6LIJlGjZ9m59emfuwnBnsIQG7DirJwe9SXrLXnexRQWqyzCdkYaOqkpKrsjuxUj2-MHX31FqsdpJJsOAvYXGOYBKJRjhGrGdONVrZdUdTBQ\",\"e\":\"AQAB\",\"x5c\":[\"MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R\"],\"issuer\":\"https://login.microsoftonline.com/{tenantid}/v2.0\"},{\"kty\":\"RSA\",\"use\":\"sig\",\"kid\":\"l3sQ-50cCH4xBVZLHTGwnSR7680\",\"x5t\":\"l3sQ-50cCH4xBVZLHTGwnSR7680\",\"n\":\"sfsXMXWuO-dniLaIELa3Pyqz9Y_rWff_AVrCAnFSdPHa8__Pmkbt_yq-6Z3u1o4gjRpKWnrjxIh8zDn1Z1RS26nkKcNg5xfWxR2K8CPbSbY8gMrp_4pZn7tgrEmoLMkwfgYaVC-4MiFEo1P2gd9mCdgIICaNeYkG1bIPTnaqquTM5KfT971MpuOVOdM1ysiejdcNDvEb7v284PYZkw2imwqiBY3FR0sVG7jgKUotFvhd7TR5WsA20GS_6ZIkUUlLUbG_rXWGl0YjZLS_Uf4q8Hbo7u-7MaFn8B69F6YaFdDlXm_A0SpedVFWQFGzMsp43_6vEzjfrFDJVAYkwb6xUQ\",\"e\":\"AQAB\",\"x5c\":[\"MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ\"],\"issuer\":\"https://login.microsoftonline.com/{tenantid}/v2.0\"},{\"kty\":\"RSA\",\"use\":\"sig\",\"kid\":\"Mr5-AUibfBii7Nd1jBebaxboXW0\",\"x5t\":\"Mr5-AUibfBii7Nd1jBebaxboXW0\",\"n\":\"yr3v1uETrFfT17zvOiy01w8nO-1t67cmiZLZxq2ISDdte9dw-IxCR7lPV2wezczIRgcWmYgFnsk2j6m10H4tKzcqZM0JJ_NigY29pFimxlL7_qXMB1PorFJdlAKvp5SgjSTwLrXjkr1AqWwbpzG2yZUNN3GE8GvmTeo4yweQbNCd-yO_Zpozx0J34wHBEMuaw-ZfCUk7mdKKsg-EcE4Zv0Xgl9wP2MpKPx0V8gLazxe6UQ9ShzNuruSOncpLYJN_oQ4aKf5ptOp1rsfDY2IK9frtmRTKOdQ-MEmSdjGL_88IQcvCs7jqVz53XKoXRlXB8tMIGOcg-ICer6yxe2itIQ\",\"e\":\"AQAB\",\"x5c\":[\"MIIDBTCCAe2gAwIBAgIQff8yrFO3CINPHUTT76tUsTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMTAyNDE3NDU1NloXDTI2MTAyNDE3NDU1NlowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMq979bhE6xX09e87zostNcPJzvtbeu3JomS2catiEg3bXvXcPiMQke5T1dsHs3MyEYHFpmIBZ7JNo+ptdB+LSs3KmTNCSfzYoGNvaRYpsZS+/6lzAdT6KxSXZQCr6eUoI0k8C6145K9QKlsG6cxtsmVDTdxhPBr5k3qOMsHkGzQnfsjv2aaM8dCd+MBwRDLmsPmXwlJO5nSirIPhHBOGb9F4J" <> ..., + headers: [ + {"Cache-Control", "max-age=86400, private"}, + {"Content-Type", "application/json; charset=utf-8"}, + {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, + {"X-Content-Type-Options", "nosniff"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "GET, OPTIONS"}, + {"P3P", "CP=\"DSP CUR OTPi IND OTRi ONL FIN\""}, + {"x-ms-request-id", "ec143cc1-4f0e-4963-ad77-2402a1912000"}, + {"x-ms-ests-server", "2.1.14059.13 - WUS2 ProdSlices"}, + {"X-XSS-Protection", "0"}, + {"Set-Cookie", + "fpc=ApJYMLn0O4NKmq2CR0m5TyM; expires=Mon, 12-Dec-2022 19:37:11 GMT; path=/; secure; HttpOnly; SameSite=None"}, + {"Set-Cookie", + "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7WevrTXMM7fylmF8MD4TGGjPNwE2OY80sEGX5uGYrl3tD7knNeyForNZxSBE5yNLtiL-pgqiNIJI7OLB_bKoDbEGLyfmMywO4uV-D85zH0ozid5Vs6baZRFIaVnawAV489q1Roo_LZdPreHs0XchjCsGN6ihHRTUYyYvaiDQFymddcs3b7lhdt5sMHHMdC2V610ZYEZWJ4VcbCuzjnSW4PULqL21PgLYkfj4bhtrZwBGTFl0gAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None"}, + {"Set-Cookie", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly"}, + {"Set-Cookie", + "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"}, + {"Date", "Sat, 12 Nov 2022 19:37:11 GMT"}, + {"Content-Length", "15922"} + ], + request_url: "https://login.microsoftonline.com/common/discovery/v2.0/keys", + request: %HTTPoison.Request{ + method: :get, + url: "https://login.microsoftonline.com/common/discovery/v2.0/keys", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/google/discovery_document.exs b/test/fixtures/http/google/discovery_document.exs new file mode 100644 index 0000000..0a9a5b4 --- /dev/null +++ b/test/fixtures/http/google/discovery_document.exs @@ -0,0 +1,36 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\n \"issuer\": \"https://accounts.google.com\",\n \"authorization_endpoint\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"device_authorization_endpoint\": \"https://oauth2.googleapis.com/device/code\",\n \"token_endpoint\": \"https://oauth2.googleapis.com/token\",\n \"userinfo_endpoint\": \"https://openidconnect.googleapis.com/v1/userinfo\",\n \"revocation_endpoint\": \"https://oauth2.googleapis.com/revoke\",\n \"jwks_uri\": \"https://www.googleapis.com/oauth2/v3/certs\",\n \"response_types_supported\": [\n \"code\",\n \"token\",\n \"id_token\",\n \"code token\",\n \"code id_token\",\n \"token id_token\",\n \"code token id_token\",\n \"none\"\n ],\n \"subject_types_supported\": [\n \"public\"\n ],\n \"id_token_signing_alg_values_supported\": [\n \"RS256\"\n ],\n \"scopes_supported\": [\n \"openid\",\n \"email\",\n \"profile\"\n ],\n \"token_endpoint_auth_methods_supported\": [\n \"client_secret_post\",\n \"client_secret_basic\"\n ],\n \"claims_supported\": [\n \"aud\",\n \"email\",\n \"email_verified\",\n \"exp\",\n \"family_name\",\n \"given_name\",\n \"iat\",\n \"iss\",\n \"locale\",\n \"name\",\n \"picture\",\n \"sub\"\n ],\n \"code_challenge_methods_supported\": [\n \"plain\",\n \"S256\"\n ],\n \"grant_types_supported\": [\n \"authorization_code\",\n \"refresh_token\",\n \"urn:ietf:params:oauth:grant-type:device_code\",\n \"urn:ietf:params:oauth:grant-type:jwt-bearer\"\n ]\n}\n", + headers: [ + {"Accept-Ranges", "bytes"}, + {"Vary", "Accept-Encoding"}, + {"Access-Control-Allow-Origin", "*"}, + {"Content-Security-Policy-Report-Only", + "require-trusted-types-for 'script'; report-uri https://csp.withgoogle.com/csp/federated-signon-mpm-access"}, + {"Cross-Origin-Opener-Policy", + "same-origin; report-to=\"federated-signon-mpm-access\""}, + {"Report-To", + "{\"group\":\"federated-signon-mpm-access\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/federated-signon-mpm-access\"}]}"}, + {"Content-Length", "1280"}, + {"X-Content-Type-Options", "nosniff"}, + {"Server", "sffe"}, + {"X-XSS-Protection", "0"}, + {"Date", "Sat, 12 Nov 2022 19:03:58 GMT"}, + {"Expires", "Sat, 12 Nov 2022 20:03:58 GMT"}, + {"Cache-Control", "public, max-age=3600"}, + {"Age", "1698"}, + {"Last-Modified", "Thu, 16 Jan 2020 21:53:16 GMT"}, + {"Content-Type", "application/json"}, + {"Alt-Svc", + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\""} + ], + request_url: "https://accounts.google.com/.well-known/openid-configuration", + request: %HTTPoison.Request{ + method: :get, + url: "https://accounts.google.com/.well-known/openid-configuration", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/google/jwks.exs b/test/fixtures/http/google/jwks.exs new file mode 100644 index 0000000..194e45c --- /dev/null +++ b/test/fixtures/http/google/jwks.exs @@ -0,0 +1,29 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\n \"keys\": [\n {\n \"use\": \"sig\",\n \"kid\": \"f451345fad08101bfb345cf642a2da9267b9ebeb\",\n \"kty\": \"RSA\",\n \"alg\": \"RS256\",\n \"n\": \"ppFPAZUqIVqCf_SffT6xDCXu1R7aRoT6TNT5_Q8PKxkkqbOVysJPNwliF-486VeM8KNW8onFOv0GkP0lJ2ASrVgyMG1qmlGUlKug64dMQXPxSlVUCXCPN676W5IZTvT0tD2byM_29HZXnOifRg-d7PRRvIBLSUWe-fGb1-tP2w65SOW-W6LuOjGzLNPJFYQvHyUx_uXHOCfIoSb8kaMwx8bCWvKc76yT0DG1wcygGXKuFQHW-Sdi1j_6bF19lVu30DX-jhYsNMUnGUr6g2iycQ50pWMORZqvcHVOH1bbDrWuz0b564sK0ET2B3XDR37djNQ305PxiQZaBStm-hM8Aw\",\n \"e\": \"AQAB\"\n },\n {\n \"n\": \"z8PS6saDU3h5ZbQb3Lwl_Arwgu65ECMi79KUlzx4tqk8bgxtaaHcqyvWqVdsA9H6Q2ZtQhBZivqV4Jg0HoPHcEwv46SEziFQNR2LH86e-WIDI5pk2NKg_9cFMee9Mz7f_NSQJ3uyD1pu86bdUTYhCw57DbEVDOuubClNMUV456dWx7dx5W4kdcQe63vGg9LXQ-9PPz9AL-0ZKr8eQEHp4KRfRUfngjqjYBMTFuuo38l94KR99B04Z-FboGnqYLgNxctwZ9eXbCerb9bV5-Q9Gb3zoo0x1h90tFdgmC2ZU1xcIIjHmFqJ29mSDZHYAAYtMNAeWreK4gqWJunc9o0vpQ\",\n \"kty\": \"RSA\",\n \"alg\": \"RS256\",\n \"kid\": \"713fd68c966e29380981edc0164a2f6c06c5702a\",\n \"use\": \"sig\",\n \"e\": \"AQAB\"\n }\n ]\n}\n", + headers: [ + {"Server", "scaffolding on HTTPServer2"}, + {"X-XSS-Protection", "0"}, + {"X-Frame-Options", "SAMEORIGIN"}, + {"X-Content-Type-Options", "nosniff"}, + {"Date", "Sat, 12 Nov 2022 19:33:44 GMT"}, + {"Expires", "Sun, 13 Nov 2022 01:32:36 GMT"}, + {"Cache-Control", "public, max-age=21532, must-revalidate, no-transform"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Age", "5"}, + {"Alt-Svc", + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\""}, + {"Accept-Ranges", "none"}, + {"Vary", "Origin,X-Origin,Referer,Accept-Encoding"}, + {"Transfer-Encoding", "chunked"} + ], + request_url: "https://www.googleapis.com/oauth2/v3/certs", + request: %HTTPoison.Request{ + method: :get, + url: "https://www.googleapis.com/oauth2/v3/certs", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/keycloak/discovery_document.exs b/test/fixtures/http/keycloak/discovery_document.exs new file mode 100644 index 0000000..42bae97 --- /dev/null +++ b/test/fixtures/http/keycloak/discovery_document.exs @@ -0,0 +1,23 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"issuer\":\"http://localhost:8080/realms/master\",\"authorization_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/auth\",\"token_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/token\",\"introspection_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/token/introspect\",\"userinfo_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/userinfo\",\"end_session_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/logout\",\"frontchannel_logout_session_supported\":true,\"frontchannel_logout_supported\":true,\"jwks_uri\":\"http://localhost:8080/realms/master/protocol/openid-connect/certs\",\"check_session_iframe\":\"http://localhost:8080/realms/master/protocol/openid-connect/login-status-iframe.html\",\"grant_types_supported\":[\"authorization_code\",\"implicit\",\"refresh_token\",\"password\",\"client_credentials\",\"urn:ietf:params:oauth:grant-type:device_code\",\"urn:openid:params:grant-type:ciba\"],\"acr_values_supported\":[\"0\",\"1\"],\"response_types_supported\":[\"code\",\"none\",\"id_token\",\"token\",\"id_token token\",\"code id_token\",\"code token\",\"code id_token token\"],\"subject_types_supported\":[\"public\",\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"id_token_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"id_token_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"userinfo_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\",\"none\"],\"userinfo_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"userinfo_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"request_object_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\",\"none\"],\"request_object_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"request_object_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\",\"query.jwt\",\"fragment.jwt\",\"form_post.jwt\",\"jwt\"],\"registration_endpoint\":\"http://localhost:8080/realms/master/clients-registrations/openid-connect\",\"token_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_secret_basic\",\"client_secret_post\",\"tls_client_auth\",\"client_secret_jwt\"],\"token_endpoint_auth_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"introspection_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_secret_basic\",\"client_secret_post\",\"tls_client_auth\",\"client_secret_jwt\"],\"introspection_endpoint_auth_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"authorization_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"authorization_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"authorization_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"claims_supported\":[\"aud\",\"sub\",\"iss\",\"auth_time\",\"name\",\"given_name\",\"family_name\",\"preferred_username\",\"email\",\"acr\"],\"claim_types_supported\":[\"normal\"],\"claims_parameter_supported\":true,\"scopes_supported\":[\"openid\",\"phone\",\"acr\",\"microprofile-jwt\",\"email\",\"profile\",\"web-origins\",\"offline_access\",\"address\",\"roles\"],\"request_parameter_supported\":true,\"request_uri_parameter_supported\":true,\"require_request_uri_registration\":true,\"code_challenge_methods_supported\":[\"plain\",\"S256\"],\"tls_client_certificate_bound_access_tokens\":true,\"revocation_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/revoke\",\"revocation_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_" <> ..., + headers: [ + {"Referrer-Policy", "no-referrer"}, + {"X-Frame-Options", "SAMEORIGIN"}, + {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, + {"Cache-Control", "no-cache, must-revalidate, no-transform, no-store"}, + {"X-Content-Type-Options", "nosniff"}, + {"X-XSS-Protection", "1; mode=block"}, + {"Content-Type", "application/json"}, + {"content-length", "5823"} + ], + request_url: "http://localhost:8080/realms/master/.well-known/openid-configuration", + request: %HTTPoison.Request{ + method: :get, + url: "http://localhost:8080/realms/master/.well-known/openid-configuration", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/keycloak/jwks.exs b/test/fixtures/http/keycloak/jwks.exs new file mode 100644 index 0000000..e3cd47f --- /dev/null +++ b/test/fixtures/http/keycloak/jwks.exs @@ -0,0 +1,23 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"keys\":[{\"kid\":\"nB0vgzwAcgJmjXwQZSzBMNkhoCaH4fvSwx5GC8Jq93c\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"use\":\"sig\",\"n\":\"zTxXhjNpLJy13O1sqVqZnlbqB0U618c9micjrs2f4NzdPT7rwLRnG3TYgTgMLDN8ERLffw-5RLZAOvTyryC0JLL2KhM-n6myrpJ5vjemp-l4f-RpcgtEVx1pe8ylKpj3SytZglMBC8ds8zFHMMf3y2HrqRUPfPHKCmdpRkLs7PhEBv8A3OTgCtp-g1YUB4s46vRun5AutMDogEXUMHdgkwPjTPTTRoUOiSulb5enhKIYj3xxUsK3yTT3Y6KkHuiHUvEfgX4l7ZsEAk1U1nA_-u86QlONRu4XVloiOHEU18zoFn6-xhER1j6lX000zTPf2FSbPpL2GdTPCN7Bj9t7rQ\",\"e\":\"AQAB\",\"x5c\":[\"MIICmzCCAYMCBgGEZ9w6QTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjIxMTExMTgwMTM2WhcNMzIxMTExMTgwMzE2WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNPFeGM2ksnLXc7WypWpmeVuoHRTrXxz2aJyOuzZ/g3N09PuvAtGcbdNiBOAwsM3wREt9/D7lEtkA69PKvILQksvYqEz6fqbKuknm+N6an6Xh/5GlyC0RXHWl7zKUqmPdLK1mCUwELx2zzMUcwx/fLYeupFQ988coKZ2lGQuzs+EQG/wDc5OAK2n6DVhQHizjq9G6fkC60wOiARdQwd2CTA+NM9NNGhQ6JK6Vvl6eEohiPfHFSwrfJNPdjoqQe6IdS8R+BfiXtmwQCTVTWcD/67zpCU41G7hdWWiI4cRTXzOgWfr7GERHWPqVfTTTNM9/YVJs+kvYZ1M8I3sGP23utAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALXouAkGrzu+5EpJLWwfQdtEYuEwK9VNheDdR2WqQDNwduk71hZWr5olNyhJz6ogMPOcNhQ33B9MssYqKtINrgxeSrO5k2QAYRYRq4BEJ+3Y9Yif9KNYV7IBXcNz4Z6fiFUnmIPoOU7dCb1Lt8sFLaa5EL1TVKQkMJ5nXLpMtTHywwEPvb/Aul1ssydfanpDW4/nCf512UpsnWetfiKw+IlTB7Rt5zMQ65RAimxz+hnY2+LF/XGnyNnPY6IPvX9suyt6u3EBM2xdXD+2UedJ8EbSvPTVo9nPfH59HmeIrIyJcw6/4xEjEVWN8oQEeYM3VOACORr2yK7zqbVpHQvOcz4=\"],\"x5t\":\"JZ8jwBZn8nzU0Rl33DcHwZU_Qio\",\"x5t#S256\":\"Y1XJpsaLUg3zbJVHyJij-zkNWmTbnM0y0Er1No84uTE\"},{\"kid\":\"Q2O_f0Z7hVL1WUxKQbChyWK_FzQK-qRZh4Kg9mxxt_I\",\"kty\":\"RSA\",\"alg\":\"RSA-OAEP\",\"use\":\"enc\",\"n\":\"jY-UVOgGl1Io_aL8jS8__Y25tqsefFfjR-JIcd3fhMjHWfoomfPlz0YfUHC6UYF7dgQeQnFEQqjxonJLDh32EWKWuXvcEvfp5592tx6COsOku_jeypwNurj9iGkbx3bv8w7x-SVp5VLdCM0IXBASIVTdmOwVfMJChIrJbjsk_wgCyL2IzU4w5cb2NdodEf1cf5ROt25EhdVZhvFzcxsfHaqOvKPBtP1W3FXbuVAIkFuXxSZdAKOZHS00RX_YOFIcOr5USIof9lBF_fXo7UXY-gDz95MkwgnfbC6WnVk4v57fniytwCNwZO3Smt3WTDBhAeFp9d8Xn_sUhwoqcBw_9Q\",\"e\":\"AQAB\",\"x5c\":[\"MIICmzCCAYMCBgGEZ9w7jTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjIxMTExMTgwMTM3WhcNMzIxMTExMTgwMzE3WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCNj5RU6AaXUij9ovyNLz/9jbm2qx58V+NH4khx3d+EyMdZ+iiZ8+XPRh9QcLpRgXt2BB5CcURCqPGicksOHfYRYpa5e9wS9+nnn3a3HoI6w6S7+N7KnA26uP2IaRvHdu/zDvH5JWnlUt0IzQhcEBIhVN2Y7BV8wkKEisluOyT/CALIvYjNTjDlxvY12h0R/Vx/lE63bkSF1VmG8XNzGx8dqo68o8G0/VbcVdu5UAiQW5fFJl0Ao5kdLTRFf9g4Uhw6vlRIih/2UEX99ejtRdj6APP3kyTCCd9sLpadWTi/nt+eLK3AI3Bk7dKa3dZMMGEB4Wn13xef+xSHCipwHD/1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD0e9ia9dDegLJFjRdAtfMZXj+6nVfSPNSPgMp4FBa1lx98EP5B3cEapVDRoAr/q3W3sI/88mzzXhrjhZEENep02JcKTZdZfyRoPShyVcPTLLUH0iiRYszTYj05iUpB9/wETN7rAjqpP+CV2a5uUL14K4sPZeWKOx3wCjEl7AdzlWCc65/XB1ZRVrRF0zJPcKQWb0YWgJb5cbj6/PNR3ZCHUw+PYi+i3/lJ3XObXmv/5+2PP0eXmeo9eTxoctKN947He95ugsOekzB2nU1XNcxDZzlMvKD2OiwkuG9SM+Uw7/sTBf/X/pHfzF9sKeq7B0vtHDunm+uBvRTZfbrDYKWk=\"],\"x5t\":\"Y8cA4ZtbCbhVKJWC4swg2H3oBRE\",\"x5t#S256\":\"pYO4_DMcglAP1G5HUhZxwlTo_nnDTpt7ORinPpUEiRc\"}]}", + headers: [ + {"Referrer-Policy", "no-referrer"}, + {"X-Frame-Options", "SAMEORIGIN"}, + {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, + {"Cache-Control", "no-cache"}, + {"X-Content-Type-Options", "nosniff"}, + {"X-XSS-Protection", "1; mode=block"}, + {"Content-Type", "application/json"}, + {"content-length", "2917"} + ], + request_url: "http://localhost:8080/realms/master/protocol/openid-connect/certs", + request: %HTTPoison.Request{ + method: :get, + url: "http://localhost:8080/realms/master/protocol/openid-connect/certs", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/okta/discovery_document.exs b/test/fixtures/http/okta/discovery_document.exs new file mode 100644 index 0000000..7e646ab --- /dev/null +++ b/test/fixtures/http/okta/discovery_document.exs @@ -0,0 +1,34 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"issuer\":\"https://common.okta.com\",\"authorization_endpoint\":\"https://common.okta.com/oauth2/v1/authorize\",\"token_endpoint\":\"https://common.okta.com/oauth2/v1/token\",\"userinfo_endpoint\":\"https://common.okta.com/oauth2/v1/userinfo\",\"registration_endpoint\":\"https://common.okta.com/oauth2/v1/clients\",\"jwks_uri\":\"https://common.okta.com/oauth2/v1/keys\",\"response_types_supported\":[\"code\",\"id_token\",\"code id_token\",\"code token\",\"id_token token\",\"code id_token token\"],\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\",\"okta_post_message\"],\"grant_types_supported\":[\"authorization_code\",\"implicit\",\"refresh_token\",\"password\",\"urn:ietf:params:oauth:grant-type:device_code\"],\"subject_types_supported\":[\"public\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"scopes_supported\":[\"openid\",\"email\",\"profile\",\"address\",\"phone\",\"offline_access\",\"groups\"],\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"none\"],\"claims_supported\":[\"iss\",\"ver\",\"sub\",\"aud\",\"iat\",\"exp\",\"jti\",\"auth_time\",\"amr\",\"idp\",\"nonce\",\"name\",\"nickname\",\"preferred_username\",\"given_name\",\"middle_name\",\"family_name\",\"email\",\"email_verified\",\"profile\",\"zoneinfo\",\"locale\",\"address\",\"phone_number\",\"picture\",\"website\",\"gender\",\"birthdate\",\"updated_at\",\"at_hash\",\"c_hash\"],\"code_challenge_methods_supported\":[\"S256\"],\"introspection_endpoint\":\"https://common.okta.com/oauth2/v1/introspect\",\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"none\"],\"revocation_endpoint\":\"https://common.okta.com/oauth2/v1/revoke\",\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"none\"],\"end_session_endpoint\":\"https://common.okta.com/oauth2/v1/logout\",\"request_parameter_supported\":true,\"request_object_signing_alg_values_supported\":[\"HS256\",\"HS384\",\"HS512\",\"RS256\",\"RS384\",\"RS512\",\"ES256\",\"ES384\",\"ES512\"],\"device_authorization_endpoint\":\"https://common.okta.com/oauth2/v1/device/authorize\"}", + headers: [ + {"Date", "Sat, 12 Nov 2022 19:40:24 GMT"}, + {"Content-Type", "application/json"}, + {"Transfer-Encoding", "chunked"}, + {"Connection", "keep-alive"}, + {"Server", "nginx"}, + {"Public-Key-Pins-Report-Only", + "pin-sha256=\"r5EfzZxQVvQpKo3AgYRaT7X2bDO/kj3ACwmxfdT2zt8=\"; pin-sha256=\"MaqlcUgk2mvY/RFSGeSwBRkI+rZ6/dxe/DuQfBT/vnQ=\"; pin-sha256=\"72G5IEvDEWn+EThf3qjR7/bQSWaS2ZSLqolhnO6iyJI=\"; pin-sha256=\"rrV6CLCCvqnk89gWibYT0JO6fNQ8cCit7GGoiVTjCOg=\"; max-age=60; report-uri=\"https://okta.report-uri.com/r/default/hpkp/reportOnly\""}, + {"x-xss-protection", "0"}, + {"p3p", "CP=\"HONK\""}, + {"content-security-policy", + "default-src 'self' common.okta.com *.oktacdn.com; connect-src 'self' common.okta.com common-admin.okta.com *.oktacdn.com *.mixpanel.com *.mapbox.com app.pendo.io data.pendo.io pendo-static-5634101834153984.storage.googleapis.com pendo-static-5391521872216064.storage.googleapis.com common.kerberos.okta.com https://oinmanager.okta.com data:; script-src 'unsafe-inline' 'unsafe-eval' 'self' common.okta.com *.oktacdn.com; style-src 'unsafe-inline' 'self' common.okta.com *.oktacdn.com app.pendo.io cdn.pendo.io pendo-static-5634101834153984.storage.googleapis.com pendo-static-5391521872216064.storage.googleapis.com; frame-src 'self' common.okta.com common-admin.okta.com login.okta.com; img-src 'self' common.okta.com *.oktacdn.com *.tiles.mapbox.com *.mapbox.com app.pendo.io data.pendo.io cdn.pendo.io pendo-static-5634101834153984.storage.googleapis.com pendo-static-5391521872216064.storage.googleapis.com data: blob:; font-src 'self' common.okta.com data: *.oktacdn.com fonts.gstatic.com; frame-ancestors 'self'"}, + {"expect-ct", + "report-uri=\"https://oktaexpectct.report-uri.com/r/t/ct/reportOnly\", max-age=0"}, + {"cache-control", "max-age=86400, must-revalidate"}, + {"expires", "Sun, 13 Nov 2022 19:40:24 GMT"}, + {"vary", "Origin"}, + {"x-content-type-options", "nosniff"}, + {"Strict-Transport-Security", "max-age=315360000; includeSubDomains"}, + {"X-Okta-Request-Id", "Y2_2qFFQ3zhcoZh3312xKwAAARU"} + ], + request_url: "https://common.okta.com/.well-known/openid-configuration", + request: %HTTPoison.Request{ + method: :get, + url: "https://common.okta.com/.well-known/openid-configuration", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/okta/jwks.exs b/test/fixtures/http/okta/jwks.exs new file mode 100644 index 0000000..7798981 --- /dev/null +++ b/test/fixtures/http/okta/jwks.exs @@ -0,0 +1,36 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"keys\":[{\"kty\":\"RSA\",\"alg\":\"RS256\",\"kid\":\"sljES1Bh0VMGwUpPfFunCVAzvLRdea7WluQfeV6zWTQ\",\"use\":\"sig\",\"e\":\"AQAB\",\"n\":\"7d7C4UL4HujzZesiEOtQUZcusHzBoUJVJXarHz0x9vMzQ1PYaGwivWJimnBHQXw6r1T05PQxOik9NnxvtPF7snPxVzDtDgrqzjd3WoYWmFiWrJz1vwebiioeFQKla7GkxfoE4cNFlIzi-i9y76zWwR3R3u0hUzHyY5XZcIBWnnInYKFACCNES7lqKu4qE3XTluJiP-WvDo79iFM67V2ZDowOWPLKoJQI0CA9l1Nkklaq32bjtMD9njl1Pl1KOKqZNyn1RzkmG0V15CYR959EEU7_Pl1LrrxGcgS-wafoyKILaJxEyeMWd3_SM0_anSAVvyUA46PYefcdEuURp-r4vQ\"},{\"kty\":\"RSA\",\"alg\":\"RS256\",\"kid\":\"TgT2Cxt-D-sVMlgTm3On7DLee8ljXgYhdzkrPkTc4sY\",\"use\":\"sig\",\"e\":\"AQAB\",\"n\":\"3VQaNpbZ67tiVkNqLF4j5skeys9D0Vzfu8NpOE8ZRVBmzLXa-FZ65cm6IGObMHhyDEBT4MTD3DLTRufVaiUbGcvrx5qee9eV_U3AwxSkRBEuHi-4HvUGkbvvXJpaoIHrNONZ_qLnL-GQm-kWTr3BaaRQ8lmMQjh3G4aCzzsFCpMT2HEe1GwCWDGTS_tDGt7oyueOtaPYFP3YLW7n8GW0-nVdiFxXYU0F-l9BF95YgYSut18r6xKk4EfHY4VNC6Y-qbldyEJ0iGdUT5sa07d7q6ocwDRO6iB07j65v43-A-H5vcew9N1JvFXXiJZ4Qn2UhzAGgUm6-Exr6fOko0W3zw\"}]}", + headers: [ + {"Date", "Sat, 12 Nov 2022 19:41:02 GMT"}, + {"Content-Type", "application/json"}, + {"Transfer-Encoding", "chunked"}, + {"Connection", "keep-alive"}, + {"Server", "nginx"}, + {"Public-Key-Pins-Report-Only", + "pin-sha256=\"r5EfzZxQVvQpKo3AgYRaT7X2bDO/kj3ACwmxfdT2zt8=\"; pin-sha256=\"MaqlcUgk2mvY/RFSGeSwBRkI+rZ6/dxe/DuQfBT/vnQ=\"; pin-sha256=\"72G5IEvDEWn+EThf3qjR7/bQSWaS2ZSLqolhnO6iyJI=\"; pin-sha256=\"rrV6CLCCvqnk89gWibYT0JO6fNQ8cCit7GGoiVTjCOg=\"; max-age=60; report-uri=\"https://okta.report-uri.com/r/default/hpkp/reportOnly\""}, + {"x-xss-protection", "0"}, + {"p3p", "CP=\"HONK\""}, + {"content-security-policy", + "default-src 'self' common.okta.com *.oktacdn.com; connect-src 'self' common.okta.com common-admin.okta.com *.oktacdn.com *.mixpanel.com *.mapbox.com app.pendo.io data.pendo.io pendo-static-5634101834153984.storage.googleapis.com pendo-static-5391521872216064.storage.googleapis.com common.kerberos.okta.com https://oinmanager.okta.com data:; script-src 'unsafe-inline' 'unsafe-eval' 'self' common.okta.com *.oktacdn.com; style-src 'unsafe-inline' 'self' common.okta.com *.oktacdn.com app.pendo.io cdn.pendo.io pendo-static-5634101834153984.storage.googleapis.com pendo-static-5391521872216064.storage.googleapis.com; frame-src 'self' common.okta.com common-admin.okta.com login.okta.com; img-src 'self' common.okta.com *.oktacdn.com *.tiles.mapbox.com *.mapbox.com app.pendo.io data.pendo.io cdn.pendo.io pendo-static-5634101834153984.storage.googleapis.com pendo-static-5391521872216064.storage.googleapis.com data: blob:; font-src 'self' common.okta.com data: *.oktacdn.com fonts.gstatic.com; frame-ancestors 'self'; report-uri https://oktacsp.report-uri.com/r/t/csp/enforce; report-to csp"}, + {"report-to", + "{\"group\":\"csp\",\"max_age\":31536000,\"endpoints\":[{\"url\":\"https://oktacsp.report-uri.com/a/t/g\"}],\"include_subdomains\":true}"}, + {"expect-ct", + "report-uri=\"https://oktaexpectct.report-uri.com/r/t/ct/reportOnly\", max-age=0"}, + {"cache-control", "max-age=3828277, must-revalidate"}, + {"expires", "Tue, 27 Dec 2022 03:05:39 GMT"}, + {"vary", "Origin"}, + {"x-content-type-options", "nosniff"}, + {"Strict-Transport-Security", "max-age=315360000; includeSubDomains"}, + {"X-Okta-Request-Id", "Y2_2zY7e1--88ktkBh3QSgAABkg"} + ], + request_url: "https://common.okta.com/oauth2/v1/keys", + request: %HTTPoison.Request{ + method: :get, + url: "https://common.okta.com/oauth2/v1/keys", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/onelogin/discovery_document.exs b/test/fixtures/http/onelogin/discovery_document.exs new file mode 100644 index 0000000..a5f5776 --- /dev/null +++ b/test/fixtures/http/onelogin/discovery_document.exs @@ -0,0 +1,25 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"acr_values_supported\":[\"onelogin:nist:level:1:re-auth\"],\"authorization_endpoint\":\"https://common.onelogin.com/oidc/2/auth\",\"claims_parameter_supported\":true,\"claims_supported\":[\"sub\",\"email\",\"preferred_username\",\"name\",\"updated_at\",\"given_name\",\"family_name\",\"locale\",\"groups\",\"email_verified\",\"params\",\"phone_number\",\"acr\",\"sid\",\"auth_time\",\"iss\"],\"grant_types_supported\":[\"authorization_code\",\"implicit\",\"refresh_token\",\"client_credentials\",\"password\"],\"id_token_signing_alg_values_supported\":[\"HS256\",\"RS256\",\"PS256\"],\"issuer\":\"https://common.onelogin.com/oidc/2\",\"jwks_uri\":\"https://common.onelogin.com/oidc/2/certs\",\"registration_endpoint\":\"https://common.onelogin.com/oidc/2/register\",\"request_parameter_supported\":false,\"request_uri_parameter_supported\":false,\"response_modes_supported\":[\"form_post\",\"fragment\",\"query\"],\"response_types_supported\":[\"code\",\"id_token token\",\"id_token\"],\"scopes_supported\":[\"openid\",\"name\",\"profile\",\"groups\",\"email\",\"params\",\"phone\"],\"subject_types_supported\":[\"public\"],\"token_endpoint\":\"https://common.onelogin.com/oidc/2/token\",\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"none\"],\"userinfo_endpoint\":\"https://common.onelogin.com/oidc/2/me\",\"userinfo_signing_alg_values_supported\":[\"HS256\",\"RS256\",\"PS256\"],\"code_challenge_methods_supported\":[\"S256\"],\"introspection_endpoint\":\"https://common.onelogin.com/oidc/2/token/introspection\",\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"none\"],\"revocation_endpoint\":\"https://common.onelogin.com/oidc/2/token/revocation\",\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"none\"],\"end_session_endpoint\":\"https://common.onelogin.com/oidc/2/logout\",\"claim_types_supported\":[\"normal\"]}", + headers: [ + {"Date", "Sat, 12 Nov 2022 19:41:55 GMT"}, + {"Content-Type", "application/json; charset=utf-8"}, + {"Content-Length", "1790"}, + {"Connection", "keep-alive"}, + {"vary", "Origin"}, + {"strict-transport-security", "max-age=63072000; includeSubDomains;"}, + {"x-content-type-options", "nosniff"}, + {"set-cookie", + "ol_oidc_canary_115=false; path=/; domain=.onelogin.com; HttpOnly; Secure"}, + {"cache-control", "private"} + ], + request_url: "https://common.onelogin.com/oidc/2/.well-known/openid-configuration", + request: %HTTPoison.Request{ + method: :get, + url: "https://common.onelogin.com/oidc/2/.well-known/openid-configuration", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/onelogin/jwks.exs b/test/fixtures/http/onelogin/jwks.exs new file mode 100644 index 0000000..5c93417 --- /dev/null +++ b/test/fixtures/http/onelogin/jwks.exs @@ -0,0 +1,25 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"JRcO4nxs5jgc8YdN7I2hLO4V_ql1bdoiMXmcYgHm4Hs\",\"use\":\"sig\",\"e\":\"AQAB\",\"n\":\"z8fZszkUNh1y1iSI6ZCkrwoZx1ZcFuQEngI8G_9VPjJXupqbgXedsV0YqDzQzYmdXd_lLb_OYWdyAP1FV6d2d4PfVjw4rGLqgYN5hEPFYqDEusiKtXyeh38xl37Nb8LGTX1qdstZjcXRo2YQ64W4UyuMko_TGOCxRNJg1fAfxRt1yV_ZeFV_93BMNjubV2D7kvpzaStJmYJi8A6QHqaqHaQkxAvYhJVi9XDajD3vvUlTVyOjURAnuaByA749glGBio5N9AfFTnYbHbeBOK3VJi6EJZzsuj3-5P4GUTYnSfrScs_kblaoeqt4GkExJqMZXGJTfGnX2UbYAjGHSTAoQw\",\"status\":\"active\"}]}", + headers: [ + {"Date", "Sat, 12 Nov 2022 19:42:26 GMT"}, + {"Content-Type", "application/json; charset=utf-8"}, + {"Content-Length", "466"}, + {"Connection", "keep-alive"}, + {"vary", "Origin"}, + {"strict-transport-security", "max-age=63072000; includeSubDomains;"}, + {"x-content-type-options", "nosniff"}, + {"set-cookie", + "ol_oidc_canary_115=true; path=/; domain=.onelogin.com; HttpOnly; Secure"}, + {"cache-control", "private"} + ], + request_url: "https://common.onelogin.com/oidc/2/certs", + request: %HTTPoison.Request{ + method: :get, + url: "https://common.onelogin.com/oidc/2/certs", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/vault/discovery_document.exs b/test/fixtures/http/vault/discovery_document.exs new file mode 100644 index 0000000..c2a2c84 --- /dev/null +++ b/test/fixtures/http/vault/discovery_document.exs @@ -0,0 +1,20 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"issuer\":\"http://0.0.0.0:8200/v1/identity/oidc/provider/default\",\"jwks_uri\":\"http://0.0.0.0:8200/v1/identity/oidc/provider/default/.well-known/keys\",\"authorization_endpoint\":\"http://0.0.0.0:8200/ui/vault/identity/oidc/provider/default/authorize\",\"token_endpoint\":\"http://0.0.0.0:8200/v1/identity/oidc/provider/default/token\",\"userinfo_endpoint\":\"http://0.0.0.0:8200/v1/identity/oidc/provider/default/userinfo\",\"request_parameter_supported\":false,\"request_uri_parameter_supported\":false,\"id_token_signing_alg_values_supported\":[\"RS256\",\"RS384\",\"RS512\",\"ES256\",\"ES384\",\"ES512\",\"EdDSA\"],\"response_types_supported\":[\"code\"],\"scopes_supported\":[\"openid\"],\"claims_supported\":[],\"subject_types_supported\":[\"public\"],\"grant_types_supported\":[\"authorization_code\"],\"token_endpoint_auth_methods_supported\":[\"none\",\"client_secret_basic\",\"client_secret_post\"]}", + headers: [ + {"Cache-Control", "max-age=3600"}, + {"Content-Type", "application/json"}, + {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, + {"Date", "Sat, 12 Nov 2022 19:50:54 GMT"}, + {"Content-Length", "849"} + ], + request_url: "http://127.0.0.1:8200/v1/identity/oidc/provider/default/.well-known/openid-configuration", + request: %HTTPoison.Request{ + method: :get, + url: "http://127.0.0.1:8200/v1/identity/oidc/provider/default/.well-known/openid-configuration", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/http/vault/jwks.exs b/test/fixtures/http/vault/jwks.exs new file mode 100644 index 0000000..13b9692 --- /dev/null +++ b/test/fixtures/http/vault/jwks.exs @@ -0,0 +1,20 @@ +%HTTPoison.Response{ + status_code: 200, + body: "{\"keys\":[{\"use\":\"sig\",\"kty\":\"RSA\",\"kid\":\"cfdb8380-cbe8-a9f6-fcd4-51abcec905ce\",\"alg\":\"RS256\",\"n\":\"93riTBOuVsRDQPoXK-mJSDxbKj_m2nBZH6k47wFAzo1qPkcQqz6pcJMPLAZgBMuKUjXi0BNPwn7FO0jzyLA2dnMrk1Mu3qBBKfC6gD0TPe5r4EaQvtCuHzVr8sHd6io1FIhG4d7VmYK7wAtIwGwBix_NS7qitZsi-B8JlkttDXa_HsllB_81OCdgRctyHt3Up5RE7hjMEOn8kQv_UCSIexZ3GGQ4adkDe9Ufq-pfQCaBWxhxr6Ekr_P2beb-YWuEBlJdTFRR6jcluei5LFbseLcg5cYCfACNH10GAlURskHXXbfNrC8-9I7fgTlcntSSnsd0qFN-P6FQBd4aSwF1_Q\",\"e\":\"AQAB\"},{\"use\":\"sig\",\"kty\":\"RSA\",\"kid\":\"5e18c268-79c7-b761-16fd-3b241fa33336\",\"alg\":\"RS256\",\"n\":\"0YkIT7NWG0cWdIpV8GqPpn8b7x7N6AbmT1ZTvc-ozzVa8O-x4hscV2wCl1_UYjWwVVYQAUqg6NeqWTk3MGHfzTAxcSQALV4wMJkG98fCcazQ3KZZCpMfnaZymW5mEZyDMaZYaqanIcAlU0s6zCw7A145ykoeGUyd6j2u1_BC1LoqSLWz-8U2iJFZH2nq7wm9lVmzb2Wwe65zvvj6rg_HajgkJ2OBOEgnErZtZoJxV0sVsP87aO-KhZk97XAu0Y8sLzh7HtMMWHV_QrNTEUvVrAFzMDgAGC9Jc6nVM3idS0mdl70SWXQE_IjCrMf5ZXOliw1G2AMPXJVjae99l8fZKw\",\"e\":\"AQAB\"}]}", + headers: [ + {"Cache-Control", "no-store"}, + {"Content-Type", "application/json"}, + {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, + {"Date", "Sat, 12 Nov 2022 19:56:52 GMT"}, + {"Content-Length", "900"} + ], + request_url: "http://0.0.0.0:8200/v1/identity/oidc/provider/default/.well-known/keys", + request: %HTTPoison.Request{ + method: :get, + url: "http://0.0.0.0:8200/v1/identity/oidc/provider/default/.well-known/keys", + headers: [], + body: "", + params: %{}, + options: [] + } +} diff --git a/test/fixtures/rsa/jwk1.exs b/test/fixtures/jwks/jwk1.exs similarity index 100% rename from test/fixtures/rsa/jwk1.exs rename to test/fixtures/jwks/jwk1.exs diff --git a/test/fixtures/rsa/jwk2.exs b/test/fixtures/jwks/jwk2.exs similarity index 100% rename from test/fixtures/rsa/jwk2.exs rename to test/fixtures/jwks/jwk2.exs diff --git a/test/fixtures/rsa/jwks.exs b/test/fixtures/jwks/jwks.exs similarity index 100% rename from test/fixtures/rsa/jwks.exs rename to test/fixtures/jwks/jwks.exs diff --git a/test/openid_connect/worker_test.exs b/test/openid_connect/worker_test.exs index 281d3a9..cc5f8aa 100644 --- a/test/openid_connect/worker_test.exs +++ b/test/openid_connect/worker_test.exs @@ -6,7 +6,7 @@ defmodule OpenIDConnect.WorkerTest do setup :verify_on_exit! @google_document Fixtures.load(:google, :discovery_document) - @google_certs Fixtures.load(:google, :certs) + @google_certs Fixtures.load(:google, :jwks) alias OpenIDConnect.{HTTPClientMock} diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index b964560..ea9642e 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -7,7 +7,7 @@ defmodule OpenIDConnectTest do setup :set_jose_json_lib @google_document Fixtures.load(:google, :discovery_document) - @google_certs Fixtures.load(:google, :certs) + @google_certs Fixtures.load(:google, :jwks) alias OpenIDConnect.{HTTPClientMock, MockWorker} @@ -68,7 +68,7 @@ defmodule OpenIDConnectTest do assert expected_document == discovery_document assert expected_jwk == jwk - assert remaining_lifetime == 16750 + assert remaining_lifetime == 21527 end test "fails during open id configuration document with HTTPoison error" do @@ -252,7 +252,7 @@ defmodule OpenIDConnectTest do ] try do - expect(HTTPClientMock, :post, fn "https://www.googleapis.com/oauth2/v4/token", + expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", {:form, ^form_body}, _headers, _opts -> @@ -282,7 +282,7 @@ defmodule OpenIDConnectTest do ] try do - expect(HTTPClientMock, :post, fn "https://www.googleapis.com/oauth2/v4/token", + expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", {:form, ^form_body}, _headers, _opts -> @@ -315,7 +315,7 @@ defmodule OpenIDConnectTest do ] try do - expect(HTTPClientMock, :post, fn "https://www.googleapis.com/oauth2/v4/token", + expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", {:form, ^form_body}, _headers, _opts -> @@ -336,7 +336,7 @@ defmodule OpenIDConnectTest do http_error = %HTTPoison.Error{reason: :nxdomain} try do - expect(HTTPClientMock, :post, fn "https://www.googleapis.com/oauth2/v4/token", + expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", {:form, _form_body}, _headers, _opts -> @@ -357,7 +357,7 @@ defmodule OpenIDConnectTest do http_error = %HTTPoison.Response{status_code: 404} try do - expect(HTTPClientMock, :post, fn "https://www.googleapis.com/oauth2/v4/token", + expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", {:form, _form_body}, _headers, _opts -> @@ -378,7 +378,7 @@ defmodule OpenIDConnectTest do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) try do - {jwk, []} = Code.eval_file("test/fixtures/rsa/jwk1.exs") + {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) claims = %{"email" => "brian@example.com"} @@ -400,7 +400,7 @@ defmodule OpenIDConnectTest do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) try do - {jwk, []} = Code.eval_file("test/fixtures/rsa/jwks.exs") + {jwk, []} = Code.eval_file("test/fixtures/jwks/jwks.exs") :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) claims = %{"email" => "brian@example.com"} @@ -424,7 +424,7 @@ defmodule OpenIDConnectTest do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) try do - {jwk, []} = Code.eval_file("test/fixtures/rsa/jwk1.exs") + {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) result = OpenIDConnect.verify(:google, "fail") @@ -438,7 +438,7 @@ defmodule OpenIDConnectTest do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) try do - {jwk, []} = Code.eval_file("test/fixtures/rsa/jwk1.exs") + {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) token = @@ -461,7 +461,7 @@ defmodule OpenIDConnectTest do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) try do - {jwk, []} = Code.eval_file("test/fixtures/rsa/jwk1.exs") + {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) token = @@ -484,8 +484,8 @@ defmodule OpenIDConnectTest do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) try do - {jwk1, []} = Code.eval_file("test/fixtures/rsa/jwk1.exs") - {jwk2, []} = Code.eval_file("test/fixtures/rsa/jwk2.exs") + {jwk1, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") + {jwk2, []} = Code.eval_file("test/fixtures/jwks/jwk2.exs") :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk1)}) claims = %{"email" => "brian@example.com"} @@ -507,7 +507,7 @@ defmodule OpenIDConnectTest do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) try do - {jwk, []} = Code.eval_file("test/fixtures/rsa/jwk1.exs") + {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) claims = %{"email" => "brian@example.com"} diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 30ddfe2..9468cd0 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -1,15 +1,16 @@ defmodule Fixtures do + @moduledoc """ + Helpers for loading fixtures. + + TODO: Consider adding a mix task to rehydrate this from provided + discovery_document_uris. + """ + def load(provider, type) do response = - Code.eval_file("test/fixtures/#{provider}/#{type}.exs") + Code.eval_file("test/fixtures/http/#{provider}/#{type}.exs") |> elem(0) - |> serialize() {:ok, response} end - - defp serialize(%HTTPoison.Response{body: body} = response), - do: %{response | body: Jason.encode!(body)} - - defp serialize(response), do: response end diff --git a/test/support/mock_worker.ex b/test/support/mock_worker.ex index b9fa4d9..9b43617 100644 --- a/test/support/mock_worker.ex +++ b/test/support/mock_worker.ex @@ -7,7 +7,7 @@ defmodule OpenIDConnect.MockWorker do |> Jason.decode!() |> OpenIDConnect.normalize_discovery_document() - @google_jwk Fixtures.load(:google, :certs) + @google_jwk Fixtures.load(:google, :jwks) |> elem(1) |> Map.get(:body) |> Jason.decode!() From 9e84f3bf5c3b8f104735d081b24cb863796b3ab5 Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Sat, 12 Nov 2022 13:12:39 -0800 Subject: [PATCH 04/32] Add all the goodies --- .credo.exs | 208 +++++++++++++ .github/workflows/test.yml | 85 ++++++ .pre-commit-config.yaml | 43 +++ CHANGELOG.md | 4 +- LICENSE.md | 2 +- lib/openid_connect.ex | 40 ++- mix.exs | 12 +- mix.lock | 6 + requirements.txt | 1 + .../http/auth0/discovery_document.exs | 65 ++++- test/fixtures/http/auth0/jwks.exs | 34 ++- .../http/azure/discovery_document.exs | 57 +++- test/fixtures/http/azure/jwks.exs | 149 +++++++++- .../http/google/discovery_document.exs | 48 ++- test/fixtures/http/google/jwks.exs | 23 +- .../http/keycloak/discovery_document.exs | 276 +++++++++++++++++- test/fixtures/http/keycloak/jwks.exs | 33 ++- .../fixtures/http/okta/discovery_document.exs | 107 ++++++- test/fixtures/http/okta/jwks.exs | 23 +- .../http/onelogin/discovery_document.exs | 67 ++++- test/fixtures/http/onelogin/jwks.exs | 17 +- .../http/vault/discovery_document.exs | 36 ++- test/fixtures/http/vault/jwks.exs | 23 +- test/openid_connect/worker_test.exs | 10 +- test/openid_connect_test.exs | 8 +- test/support/fixtures.ex | 6 + test/support/jason_encoder.ex | 4 + test/support/mock_worker.ex | 3 + 28 files changed, 1317 insertions(+), 73 deletions(-) create mode 100644 .credo.exs create mode 100644 .github/workflows/test.yml create mode 100644 .pre-commit-config.yaml create mode 100644 requirements.txt diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..a7e9373 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,208 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 2]}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 0]}, + {Credo.Check.Design.TagFIXME, []}, + + # + ## Readability Checks + # + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.WrongTestFileExtension, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.UnsafeExec, []} + ], + disabled: [ + {Credo.Check.Readability.AliasOrder, []}, + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ca4c6fc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,85 @@ +name: Test +on: [push, pull_request] + +jobs: + static-analysis: + runs-on: ubuntu-latest + env: + MIX_ENV: dev + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - uses: erlef/setup-beam@v1 + with: + otp-version: '25' + elixir-version: '1.14' + - uses: actions/cache@v3.0.11 + name: Setup Elixir cache + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-otp-25-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix-otp-25- + - uses: actions/cache@v3.0.11 + name: Setup Python cache + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install Elixir Dependencies + run: mix deps.get --only dev + - name: Install Python Dependencies + run: | + pip install -r requirements.txt + # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones + # Cache key based on Elixir & Erlang version (also usefull when running in matrix) + - name: Restore PLT cache + uses: actions/cache@v3.0.11 + id: plt_cache + with: + key: | + ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt + restore-keys: | + ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt + path: | + priv/plts + # Create PLTs if no cache was found + - name: Create PLTs + if: steps.plt_cache.outputs.cache-hit != 'true' + run: mix dialyzer --plt + - name: Run pre-commit + run: | + pre-commit install + SKIP=no-commit-to-branch pre-commit run --all-files + + unit-test: + runs-on: ubuntu-latest + env: + MIX_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: '25' + elixir-version: '1.14' + - uses: actions/cache@v3.0.11 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-otp-25-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix-otp-25- + - name: Install Dependencies + run: mix deps.get --only test + - name: Run Tests and Upload Coverage Report + run: | + # XXX: This can fail when coveralls is down + mix coveralls.github diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..198f719 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +repos: + - repo: local + hooks: + # Elixir config + # Randomly started failing + # - id: mix-format + # name: 'elixir: mix format' + # entry: mix format --check-formatted + # language: system + - id: mix-lint + name: 'elixir: mix credo' + entry: mix credo --strict + language: system + pass_filenames: false + files: \.exs*$ + - id: mix-analysis + name: 'elixir: mix dialyzer' + entry: mix dialyzer --format dialyxir + language: system + pass_filenames: false + files: \.exs*$ + - id: mix-compile + name: 'elixir: mix compile' + entry: mix compile --force --warnings-as-errors + language: system + pass_filenames: false + files: \.ex$ + + # Standard pre-commit hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: mixed-line-ending + args: ['--fix=lf'] + description: Forces to replace line ending by the UNIX 'lf' character. + - id: check-merge-conflict + - id: end-of-file-fixer + exclude: "^omnibus/config/patches/" + - id: trailing-whitespace + exclude: "^omnibus/config/patches/" + - id: check-merge-conflict + - id: no-commit-to-branch + args: [-b, master, -b, develop] diff --git a/CHANGELOG.md b/CHANGELOG.md index 51c5416..b11d4a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## v0.2.2 * Allow missing `claims_supported` in discovery document -* Allow overriding document params +* Allow overriding document params ## v0.2.1 * Relaxed jason version requirement @@ -20,4 +20,4 @@ ## v0.1.0 -* Initial public release \ No newline at end of file +* Initial public release diff --git a/LICENSE.md b/LICENSE.md index cceb743..8c3e3ab 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -4,4 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/openid_connect.ex b/lib/openid_connect.ex index aa6fe59..f4d7501 100644 --- a/lib/openid_connect.ex +++ b/lib/openid_connect.ex @@ -267,11 +267,9 @@ defmodule OpenIDConnect do end defp peek_protected(jwt) do - try do - {:ok, JOSE.JWS.peek_protected(jwt)} - rescue - _ -> {:error, :peek_protected} - end + {:ok, JOSE.JWS.peek_protected(jwt)} + rescue + _ -> {:error, :peek_protected} end defp do_verify(%JOSE.JWK{keys: {:jose_jwk_set, jwks}}, token_alg, jwt) do @@ -290,12 +288,10 @@ defmodule OpenIDConnect do do: JOSE.JWS.verify_strict(jwk, [token_alg], jwt) defp from_certs(certs) do - try do - {:ok, JOSE.JWK.from(certs)} - rescue - _ -> - {:error, "certificates bad format"} - end + {:ok, JOSE.JWK.from(certs)} + rescue + _ -> + {:error, "certificates bad format"} end defp discovery_document(provider, name) do @@ -334,17 +330,15 @@ defmodule OpenIDConnect do response_types_supported = response_types_supported(provider, name) - cond do - response_type in response_types_supported -> - response_type - - true -> - raise ArgumentError, - message: """ - Requested response type (#{response_type}) not supported by provider (#{provider}). - Supported types: - #{Enum.join(response_types_supported, "\n")} - """ + if response_type in response_types_supported do + response_type + else + raise ArgumentError, + message: """ + Requested response type (#{response_type}) not supported by provider (#{provider}). + Supported types: + #{Enum.join(response_types_supported, "\n")} + """ end end @@ -400,7 +394,7 @@ defmodule OpenIDConnect do @spec remaining_lifetime([{String.t(), String.t()}]) :: integer | nil defp remaining_lifetime(headers) do - with headers = Enum.into(headers, %{}), + with headers <- Enum.into(headers, %{}), {:ok, max_age} <- find_max_age(headers), {:ok, age} <- find_age(headers) do max_age - age diff --git a/mix.exs b/mix.exs index 153e8fd..a60c181 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,14 @@ defmodule OpenIDConnect.Mixfile do deps: deps(), docs: docs(), name: "OpenID Connect", - source_url: "https://github.com/DockYard/openid_connect" + source_url: "https://github.com/firezone/openid_connect", + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.post": :test, + "coveralls.html": :test + ] ] end @@ -61,7 +68,10 @@ defmodule OpenIDConnect.Mixfile do {:jason, ">= 1.0.0"}, {:jose, "~> 1.8"}, {:earmark, "~> 1.2", only: :dev}, + {:credo, "~> 1.6", only: :dev}, + {:dialyxir, "~> 1.2", only: :dev}, {:ex_doc, "~> 0.18", only: :dev}, + {:excoveralls, "~> 0.14", only: :test}, {:mox, "~> 1.0", only: :test} ] end diff --git a/mix.lock b/mix.lock index 6a81efc..ebffc44 100644 --- a/mix.lock +++ b/mix.lock @@ -1,10 +1,16 @@ %{ "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"}, + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, "cutkey": {:git, "https://github.com/potatosalad/cutkey.git", "47640d04fb4db4a0b79168d7fca0df87aaa42751", []}, + "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, "earmark": {:hex, :earmark, "1.4.33", "2b33a505180583f98bfa17317f03973b52081bdb24a11be05a7f4fa6d64dd8bf", [:mix], [{:earmark_parser, "~> 1.4.29", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "21b31363d6a0a70802cfbaf2de88355778aa76654298a072bce2e01d1858ae06"}, "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"}, + "excoveralls": {:hex, :excoveralls, "0.15.0", "ac941bf85f9f201a9626cc42b2232b251ad8738da993cf406a4290cacf562ea4", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9631912006b27eca30a2f3c93562bc7ae15980afb014ceb8147dc5cdd8f376f1"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..561cab7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pre-commit==2.20.0 diff --git a/test/fixtures/http/auth0/discovery_document.exs b/test/fixtures/http/auth0/discovery_document.exs index c22ff7c..1a5a1dc 100644 --- a/test/fixtures/http/auth0/discovery_document.exs +++ b/test/fixtures/http/auth0/discovery_document.exs @@ -1,6 +1,66 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"issuer\":\"https://common.auth0.com/\",\"authorization_endpoint\":\"https://common.auth0.com/authorize\",\"token_endpoint\":\"https://common.auth0.com/oauth/token\",\"device_authorization_endpoint\":\"https://common.auth0.com/oauth/device/code\",\"userinfo_endpoint\":\"https://common.auth0.com/userinfo\",\"mfa_challenge_endpoint\":\"https://common.auth0.com/mfa/challenge\",\"jwks_uri\":\"https://common.auth0.com/.well-known/jwks.json\",\"registration_endpoint\":\"https://common.auth0.com/oidc/register\",\"revocation_endpoint\":\"https://common.auth0.com/oauth/revoke\",\"scopes_supported\":[\"openid\",\"profile\",\"offline_access\",\"name\",\"given_name\",\"family_name\",\"nickname\",\"email\",\"email_verified\",\"picture\",\"created_at\",\"identities\",\"phone\",\"address\"],\"response_types_supported\":[\"code\",\"token\",\"id_token\",\"code token\",\"code id_token\",\"token id_token\",\"code token id_token\"],\"code_challenge_methods_supported\":[\"S256\",\"plain\"],\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\"],\"subject_types_supported\":[\"public\"],\"id_token_signing_alg_values_supported\":[\"HS256\",\"RS256\"],\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"],\"claims_supported\":[\"aud\",\"auth_time\",\"created_at\",\"email\",\"email_verified\",\"exp\",\"family_name\",\"given_name\",\"iat\",\"identities\",\"iss\",\"name\",\"nickname\",\"phone_number\",\"picture\",\"sub\"],\"request_uri_parameter_supported\":false,\"request_parameter_supported\":false}", + body: %{ + "authorization_endpoint" => "https://common.auth0.com/authorize", + "claims_supported" => [ + "aud", + "auth_time", + "created_at", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "identities", + "iss", + "name", + "nickname", + "phone_number", + "picture", + "sub" + ], + "code_challenge_methods_supported" => ["S256", "plain"], + "device_authorization_endpoint" => "https://common.auth0.com/oauth/device/code", + "id_token_signing_alg_values_supported" => ["HS256", "RS256"], + "issuer" => "https://common.auth0.com/", + "jwks_uri" => "https://common.auth0.com/.well-known/jwks.json", + "mfa_challenge_endpoint" => "https://common.auth0.com/mfa/challenge", + "registration_endpoint" => "https://common.auth0.com/oidc/register", + "request_parameter_supported" => false, + "request_uri_parameter_supported" => false, + "response_modes_supported" => ["query", "fragment", "form_post"], + "response_types_supported" => [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "token id_token", + "code token id_token" + ], + "revocation_endpoint" => "https://common.auth0.com/oauth/revoke", + "scopes_supported" => [ + "openid", + "profile", + "offline_access", + "name", + "given_name", + "family_name", + "nickname", + "email", + "email_verified", + "picture", + "created_at", + "identities", + "phone", + "address" + ], + "subject_types_supported" => ["public"], + "token_endpoint" => "https://common.auth0.com/oauth/token", + "token_endpoint_auth_methods_supported" => ["client_secret_basic", "client_secret_post"], + "userinfo_endpoint" => "https://common.auth0.com/userinfo" + }, headers: [ {"Date", "Sat, 12 Nov 2022 19:57:59 GMT"}, {"Content-Type", "application/json; charset=utf-8"}, @@ -8,8 +68,7 @@ {"Connection", "keep-alive"}, {"CF-Ray", "7691d6fb8d50f96b-SJC"}, {"Access-Control-Allow-Origin", "*"}, - {"Cache-Control", - "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"}, + {"Cache-Control", "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"}, {"Last-Modified", "Sat, 12 Nov 2022 19:57:59 GMT"}, {"Strict-Transport-Security", "max-age=31536000"}, {"Vary", "Accept-Encoding, Origin, Accept-Encoding"}, diff --git a/test/fixtures/http/auth0/jwks.exs b/test/fixtures/http/auth0/jwks.exs index ec2f2d1..e129656 100644 --- a/test/fixtures/http/auth0/jwks.exs +++ b/test/fixtures/http/auth0/jwks.exs @@ -1,6 +1,35 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"keys\":[{\"alg\":\"RS256\",\"kty\":\"RSA\",\"use\":\"sig\",\"n\":\"u4qDTgepr5ifzZOxampfmt9naaih6CgOGGdc3F4Bd9r-R4X4gkIu6S9kN76mZ3O9_q4o4G29yJrEr2bUhYl96vrK-hbFddMhTtxy6nkKqmuPj5PXV5IEBogZwzZebXqARjmOVqxhAgZPLGCoD_ebIzYTT7ozprwlbaBS7ZmRXj3vwlffRae4T-gamIcQWhGLujxOY6FsLUP4ExqAeUcftRd0pi0XGBVL1qmu4xRPLznV6Z7OVHemtgBQ4tAKvwSYQWIDKJ886Y3jhLyQukC10N0clnPVuHDPByGXDPTDvhQRN2xTU4PNjGGu_WDElzenA-8CyZf8gEJ0ZM8_j0kmkQ\",\"e\":\"AQAB\",\"kid\":\"QjYxQ0U1QTVCMzk4RTJFNEIxMERGN0NBOUVBQTk1NDEzRTcwNTVBMg\",\"x5t\":\"QjYxQ0U1QTVCMzk4RTJFNEIxMERGN0NBOUVBQTk1NDEzRTcwNTVBMg\",\"x5c\":[\"MIIC6DCCAdCgAwIBAgIJZ9K1arBHwKy5MA0GCSqGSIb3DQEBBQUAMBsxGTAXBgNVBAMTEGNvbW1vbi5hdXRoMC5jb20wHhcNMTUxMTEwMTMxNjMzWhcNMjkwNzE5MTMxNjMzWjAbMRkwFwYDVQQDExBjb21tb24uYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4qDTgepr5ifzZOxampfmt9naaih6CgOGGdc3F4Bd9r+R4X4gkIu6S9kN76mZ3O9/q4o4G29yJrEr2bUhYl96vrK+hbFddMhTtxy6nkKqmuPj5PXV5IEBogZwzZebXqARjmOVqxhAgZPLGCoD/ebIzYTT7ozprwlbaBS7ZmRXj3vwlffRae4T+gamIcQWhGLujxOY6FsLUP4ExqAeUcftRd0pi0XGBVL1qmu4xRPLznV6Z7OVHemtgBQ4tAKvwSYQWIDKJ886Y3jhLyQukC10N0clnPVuHDPByGXDPTDvhQRN2xTU4PNjGGu/WDElzenA+8CyZf8gEJ0ZM8/j0kmkQIDAQABoy8wLTAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRkWtTfBgFGEkhPxk2TeGcjty6aDTANBgkqhkiG9w0BAQUFAAOCAQEAIFbLdkw6cSXm0OQ/rzBlzM6uBWXAYXQM2HSiMQqW4b/C37LeW0upG/0FU6hWa5fbMQvAf9QSgJsG/7CrQgnjssnqvIlw5S1enj5wzFQd9yHgb/1X+e8qOT7VEXdOZXCR/8aYuLiaHuYyNQ8XMXAl/pwaJCrcq9YhA2kBjJ/rojHXdXd9DbT/TJMXNAaNNMicRN1R8OOw+JBbjxA3dMZ9qKRN89JdTyF6c2G/FTXVw4cBoJi/lVKKBXOwT4DYZtDCyOrFaFyhkop1uRzC74pxp2lRYuaKD8ev6suRJTbukcjeerCtN/AUVXd515FyL1mdS4qVJMnT8lt6NRxXqo3EaQ==\"]},{\"alg\":\"RS256\",\"kty\":\"RSA\",\"use\":\"sig\",\"n\":\"0MdUr5IDFWWPbLFM7itct-DHoAgfAD4gfCUlGlupRIk8AvDBf0allrSjznh78gn0XcCmq5MSKH6dJYAalhz5pnKESCZS0NGRijYCnP0FWdwZmPoRfMRe-O7Fw4ogMYYcWN7Iy_6HBefNvkQ91PhDksm0KpXH-yxrFC8gcbQNV14Q9bRwFq3nWmmDj5A7OvEuFmszIvZtpFbSyrCuqMHYR-Pyo-dWRUJ8egqjiOyn2LLWX0iMASJyasG6wF6cQC7k6sIYacxn_rBtuldKBBRlGboTkFl0NW9_xw4CwGxzFj_-GacCkaL7axObS34UN2bowZkXZsSE5l8v54NjalXHcw\",\"e\":\"AQAB\",\"kid\":\"Lk-5NnjByFf99cg0nxMdi\",\"x5t\":\"WaiAZeN6U1Tc9bpQHGclV01odiw\",\"x5c\":[\"MIIC+zCCAeOgAwIBAgIJKlrwnBd5HfDrMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMTEGNvbW1vbi5hdXRoMC5jb20wHhcNMjAwMzExMTg1NjM1WhcNMzMxMTE4MTg1NjM1WjAbMRkwFwYDVQQDExBjb21tb24uYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0MdUr5IDFWWPbLFM7itct+DHoAgfAD4gfCUlGlupRIk8AvDBf0allrSjznh78gn0XcCmq5MSKH6dJYAalhz5pnKESCZS0NGRijYCnP0FWdwZmPoRfMRe+O7Fw4ogMYYcWN7Iy/6HBefNvkQ91PhDksm0KpXH+yxrFC8gcbQNV14Q9bRwFq3nWmmDj5A7OvEuFmszIvZtpFbSyrCuqMHYR+Pyo+dWRUJ8egqjiOyn2LLWX0iMASJyasG6wF6cQC7k6sIYacxn/rBtuldKBBRlGboTkFl0NW9/xw4CwGxzFj/+GacCkaL7axObS34UN2bowZkXZsSE5l8v54NjalXHcwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSR1Dfrm7GcV0/c0Nbd1q950Kuz1DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAJm/bBoE60mpzzdPGrRvNdZgYEe1faPpj5AIk8h2c+Q5v1lB7olJ4Sfb4WIvFeDxQrxB0GlW8tgN2rgtABUjs5Nhc0h7sfB9MZlkQVAZjTqVFXpv1UzzwI2e0RmD4O6m6CvVE+p5bPHao5XvtgEZKL7Q6VXTLGeyQ9XTnlda2IdcZb4IGQTzrqmq6zlAGn3u4P9hlg+7GlldfC+ROKCreFkJODU+ZPr1c1m0wdZ0UDSvk71PPhlNBLP32jRUlaiL3H9ssMoW9rP0UhYAnP6hY0aC1cmeikF1GQXn4ySK5NlKRV+iA8LY/XhYrqXsSQJPn0RkSj4JRbJbP03SZWh9aOI=\"]}]}", + body: %{ + "keys" => [ + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "QjYxQ0U1QTVCMzk4RTJFNEIxMERGN0NBOUVBQTk1NDEzRTcwNTVBMg", + "kty" => "RSA", + "n" => + "u4qDTgepr5ifzZOxampfmt9naaih6CgOGGdc3F4Bd9r-R4X4gkIu6S9kN76mZ3O9_q4o4G29yJrEr2bUhYl96vrK-hbFddMhTtxy6nkKqmuPj5PXV5IEBogZwzZebXqARjmOVqxhAgZPLGCoD_ebIzYTT7ozprwlbaBS7ZmRXj3vwlffRae4T-gamIcQWhGLujxOY6FsLUP4ExqAeUcftRd0pi0XGBVL1qmu4xRPLznV6Z7OVHemtgBQ4tAKvwSYQWIDKJ886Y3jhLyQukC10N0clnPVuHDPByGXDPTDvhQRN2xTU4PNjGGu_WDElzenA-8CyZf8gEJ0ZM8_j0kmkQ", + "use" => "sig", + "x5c" => [ + "MIIC6DCCAdCgAwIBAgIJZ9K1arBHwKy5MA0GCSqGSIb3DQEBBQUAMBsxGTAXBgNVBAMTEGNvbW1vbi5hdXRoMC5jb20wHhcNMTUxMTEwMTMxNjMzWhcNMjkwNzE5MTMxNjMzWjAbMRkwFwYDVQQDExBjb21tb24uYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4qDTgepr5ifzZOxampfmt9naaih6CgOGGdc3F4Bd9r+R4X4gkIu6S9kN76mZ3O9/q4o4G29yJrEr2bUhYl96vrK+hbFddMhTtxy6nkKqmuPj5PXV5IEBogZwzZebXqARjmOVqxhAgZPLGCoD/ebIzYTT7ozprwlbaBS7ZmRXj3vwlffRae4T+gamIcQWhGLujxOY6FsLUP4ExqAeUcftRd0pi0XGBVL1qmu4xRPLznV6Z7OVHemtgBQ4tAKvwSYQWIDKJ886Y3jhLyQukC10N0clnPVuHDPByGXDPTDvhQRN2xTU4PNjGGu/WDElzenA+8CyZf8gEJ0ZM8/j0kmkQIDAQABoy8wLTAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRkWtTfBgFGEkhPxk2TeGcjty6aDTANBgkqhkiG9w0BAQUFAAOCAQEAIFbLdkw6cSXm0OQ/rzBlzM6uBWXAYXQM2HSiMQqW4b/C37LeW0upG/0FU6hWa5fbMQvAf9QSgJsG/7CrQgnjssnqvIlw5S1enj5wzFQd9yHgb/1X+e8qOT7VEXdOZXCR/8aYuLiaHuYyNQ8XMXAl/pwaJCrcq9YhA2kBjJ/rojHXdXd9DbT/TJMXNAaNNMicRN1R8OOw+JBbjxA3dMZ9qKRN89JdTyF6c2G/FTXVw4cBoJi/lVKKBXOwT4DYZtDCyOrFaFyhkop1uRzC74pxp2lRYuaKD8ev6suRJTbukcjeerCtN/AUVXd515FyL1mdS4qVJMnT8lt6NRxXqo3EaQ==" + ], + "x5t" => "QjYxQ0U1QTVCMzk4RTJFNEIxMERGN0NBOUVBQTk1NDEzRTcwNTVBMg" + }, + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "Lk-5NnjByFf99cg0nxMdi", + "kty" => "RSA", + "n" => + "0MdUr5IDFWWPbLFM7itct-DHoAgfAD4gfCUlGlupRIk8AvDBf0allrSjznh78gn0XcCmq5MSKH6dJYAalhz5pnKESCZS0NGRijYCnP0FWdwZmPoRfMRe-O7Fw4ogMYYcWN7Iy_6HBefNvkQ91PhDksm0KpXH-yxrFC8gcbQNV14Q9bRwFq3nWmmDj5A7OvEuFmszIvZtpFbSyrCuqMHYR-Pyo-dWRUJ8egqjiOyn2LLWX0iMASJyasG6wF6cQC7k6sIYacxn_rBtuldKBBRlGboTkFl0NW9_xw4CwGxzFj_-GacCkaL7axObS34UN2bowZkXZsSE5l8v54NjalXHcw", + "use" => "sig", + "x5c" => [ + "MIIC+zCCAeOgAwIBAgIJKlrwnBd5HfDrMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMTEGNvbW1vbi5hdXRoMC5jb20wHhcNMjAwMzExMTg1NjM1WhcNMzMxMTE4MTg1NjM1WjAbMRkwFwYDVQQDExBjb21tb24uYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0MdUr5IDFWWPbLFM7itct+DHoAgfAD4gfCUlGlupRIk8AvDBf0allrSjznh78gn0XcCmq5MSKH6dJYAalhz5pnKESCZS0NGRijYCnP0FWdwZmPoRfMRe+O7Fw4ogMYYcWN7Iy/6HBefNvkQ91PhDksm0KpXH+yxrFC8gcbQNV14Q9bRwFq3nWmmDj5A7OvEuFmszIvZtpFbSyrCuqMHYR+Pyo+dWRUJ8egqjiOyn2LLWX0iMASJyasG6wF6cQC7k6sIYacxn/rBtuldKBBRlGboTkFl0NW9/xw4CwGxzFj/+GacCkaL7axObS34UN2bowZkXZsSE5l8v54NjalXHcwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSR1Dfrm7GcV0/c0Nbd1q950Kuz1DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAJm/bBoE60mpzzdPGrRvNdZgYEe1faPpj5AIk8h2c+Q5v1lB7olJ4Sfb4WIvFeDxQrxB0GlW8tgN2rgtABUjs5Nhc0h7sfB9MZlkQVAZjTqVFXpv1UzzwI2e0RmD4O6m6CvVE+p5bPHao5XvtgEZKL7Q6VXTLGeyQ9XTnlda2IdcZb4IGQTzrqmq6zlAGn3u4P9hlg+7GlldfC+ROKCreFkJODU+ZPr1c1m0wdZ0UDSvk71PPhlNBLP32jRUlaiL3H9ssMoW9rP0UhYAnP6hY0aC1cmeikF1GQXn4ySK5NlKRV+iA8LY/XhYrqXsSQJPn0RkSj4JRbJbP03SZWh9aOI=" + ], + "x5t" => "WaiAZeN6U1Tc9bpQHGclV01odiw" + } + ] + }, headers: [ {"Date", "Sat, 12 Nov 2022 19:58:26 GMT"}, {"Content-Type", "application/json; charset=utf-8"}, @@ -8,8 +37,7 @@ {"Connection", "keep-alive"}, {"CF-Ray", "7691d7a5d88df96b-SJC"}, {"Access-Control-Allow-Origin", "*"}, - {"Cache-Control", - "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"}, + {"Cache-Control", "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"}, {"Last-Modified", "Sat, 12 Nov 2022 19:58:26 GMT"}, {"Strict-Transport-Security", "max-age=31536000"}, {"Vary", "Accept-Encoding, Origin, Accept-Encoding"}, diff --git a/test/fixtures/http/azure/discovery_document.exs b/test/fixtures/http/azure/discovery_document.exs index 884b532..cd7d1c8 100644 --- a/test/fixtures/http/azure/discovery_document.exs +++ b/test/fixtures/http/azure/discovery_document.exs @@ -1,6 +1,55 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"token_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/token\",\"token_endpoint_auth_methods_supported\":[\"client_secret_post\",\"private_key_jwt\",\"client_secret_basic\"],\"jwks_uri\":\"https://login.microsoftonline.com/common/discovery/v2.0/keys\",\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\"],\"subject_types_supported\":[\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"response_types_supported\":[\"code\",\"id_token\",\"code id_token\",\"id_token token\"],\"scopes_supported\":[\"openid\",\"profile\",\"email\",\"offline_access\"],\"issuer\":\"https://login.microsoftonline.com/{tenantid}/v2.0\",\"request_uri_parameter_supported\":false,\"userinfo_endpoint\":\"https://graph.microsoft.com/oidc/userinfo\",\"authorization_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\",\"device_authorization_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/devicecode\",\"http_logout_supported\":true,\"frontchannel_logout_supported\":true,\"end_session_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/logout\",\"claims_supported\":[\"sub\",\"iss\",\"cloud_instance_name\",\"cloud_instance_host_name\",\"cloud_graph_host_name\",\"msgraph_host\",\"aud\",\"exp\",\"iat\",\"auth_time\",\"acr\",\"nonce\",\"preferred_username\",\"name\",\"tid\",\"ver\",\"at_hash\",\"c_hash\",\"email\"],\"kerberos_endpoint\":\"https://login.microsoftonline.com/common/kerberos\",\"tenant_region_scope\":null,\"cloud_instance_name\":\"microsoftonline.com\",\"cloud_graph_host_name\":\"graph.windows.net\",\"msgraph_host\":\"graph.microsoft.com\",\"rbac_url\":\"https://pas.windows.net\"}", + body: %{ + "authorization_endpoint" => "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + "claims_supported" => [ + "sub", + "iss", + "cloud_instance_name", + "cloud_instance_host_name", + "cloud_graph_host_name", + "msgraph_host", + "aud", + "exp", + "iat", + "auth_time", + "acr", + "nonce", + "preferred_username", + "name", + "tid", + "ver", + "at_hash", + "c_hash", + "email" + ], + "cloud_graph_host_name" => "graph.windows.net", + "cloud_instance_name" => "microsoftonline.com", + "device_authorization_endpoint" => + "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode", + "end_session_endpoint" => "https://login.microsoftonline.com/common/oauth2/v2.0/logout", + "frontchannel_logout_supported" => true, + "http_logout_supported" => true, + "id_token_signing_alg_values_supported" => ["RS256"], + "issuer" => "https://login.microsoftonline.com/{tenantid}/v2.0", + "jwks_uri" => "https://login.microsoftonline.com/common/discovery/v2.0/keys", + "kerberos_endpoint" => "https://login.microsoftonline.com/common/kerberos", + "msgraph_host" => "graph.microsoft.com", + "rbac_url" => "https://pas.windows.net", + "request_uri_parameter_supported" => false, + "response_modes_supported" => ["query", "fragment", "form_post"], + "response_types_supported" => ["code", "id_token", "code id_token", "id_token token"], + "scopes_supported" => ["openid", "profile", "email", "offline_access"], + "subject_types_supported" => ["pairwise"], + "tenant_region_scope" => nil, + "token_endpoint" => "https://login.microsoftonline.com/common/oauth2/v2.0/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_post", + "private_key_jwt", + "client_secret_basic" + ], + "userinfo_endpoint" => "https://graph.microsoft.com/oidc/userinfo" + }, headers: [ {"Cache-Control", "max-age=86400, private"}, {"Content-Type", "application/json; charset=utf-8"}, @@ -16,10 +65,8 @@ "fpc=AuKLSwY1b3xLiInKP16p3E4; expires=Mon, 12-Dec-2022 19:36:30 GMT; path=/; secure; HttpOnly; SameSite=None"}, {"Set-Cookie", "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7Wevr2ALKzZMjPY-Tt7ffB-f_7y4AMTUR-4m-AQDAi0jJ1K4_N7dY0CZmKZdSweQPMgerZ-TeKnty43nfmYRZS2G39bKUZp5erQLwiB9rkuLis4_ee_cAZK7nh1pkqOh0_t52P9svf75Le0-ex8iyPVhexTbIROTaaYvo6Fl9DFqOtZOnmQplc6ken-ddUcLbnZRSKOTFdr03VB8oSt5gD2BBw2e5qeBuocgX0hS-W-FNbG0gAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None"}, - {"Set-Cookie", - "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly"}, - {"Set-Cookie", - "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"}, + {"Set-Cookie", "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly"}, + {"Set-Cookie", "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"}, {"Date", "Sat, 12 Nov 2022 19:36:29 GMT"}, {"Content-Length", "1547"} ], diff --git a/test/fixtures/http/azure/jwks.exs b/test/fixtures/http/azure/jwks.exs index 18db2af..e49c858 100644 --- a/test/fixtures/http/azure/jwks.exs +++ b/test/fixtures/http/azure/jwks.exs @@ -1,6 +1,139 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"keys\":[{\"kty\":\"RSA\",\"use\":\"sig\",\"kid\":\"nOo3ZDrODXEK1jKWhXslHR_KXEg\",\"x5t\":\"nOo3ZDrODXEK1jKWhXslHR_KXEg\",\"n\":\"oaLLT9hkcSj2tGfZsjbu7Xz1Krs0qEicXPmEsJKOBQHauZ_kRM1HdEkgOJbUznUspE6xOuOSXjlzErqBxXAu4SCvcvVOCYG2v9G3-uIrLF5dstD0sYHBo1VomtKxzF90Vslrkn6rNQgUGIWgvuQTxm1uRklYFPEcTIRw0LnYknzJ06GC9ljKR617wABVrZNkBuDgQKj37qcyxoaxIGdxEcmVFZXJyrxDgdXh9owRmZn6LIJlGjZ9m59emfuwnBnsIQG7DirJwe9SXrLXnexRQWqyzCdkYaOqkpKrsjuxUj2-MHX31FqsdpJJsOAvYXGOYBKJRjhGrGdONVrZdUdTBQ\",\"e\":\"AQAB\",\"x5c\":[\"MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R\"],\"issuer\":\"https://login.microsoftonline.com/{tenantid}/v2.0\"},{\"kty\":\"RSA\",\"use\":\"sig\",\"kid\":\"l3sQ-50cCH4xBVZLHTGwnSR7680\",\"x5t\":\"l3sQ-50cCH4xBVZLHTGwnSR7680\",\"n\":\"sfsXMXWuO-dniLaIELa3Pyqz9Y_rWff_AVrCAnFSdPHa8__Pmkbt_yq-6Z3u1o4gjRpKWnrjxIh8zDn1Z1RS26nkKcNg5xfWxR2K8CPbSbY8gMrp_4pZn7tgrEmoLMkwfgYaVC-4MiFEo1P2gd9mCdgIICaNeYkG1bIPTnaqquTM5KfT971MpuOVOdM1ysiejdcNDvEb7v284PYZkw2imwqiBY3FR0sVG7jgKUotFvhd7TR5WsA20GS_6ZIkUUlLUbG_rXWGl0YjZLS_Uf4q8Hbo7u-7MaFn8B69F6YaFdDlXm_A0SpedVFWQFGzMsp43_6vEzjfrFDJVAYkwb6xUQ\",\"e\":\"AQAB\",\"x5c\":[\"MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ\"],\"issuer\":\"https://login.microsoftonline.com/{tenantid}/v2.0\"},{\"kty\":\"RSA\",\"use\":\"sig\",\"kid\":\"Mr5-AUibfBii7Nd1jBebaxboXW0\",\"x5t\":\"Mr5-AUibfBii7Nd1jBebaxboXW0\",\"n\":\"yr3v1uETrFfT17zvOiy01w8nO-1t67cmiZLZxq2ISDdte9dw-IxCR7lPV2wezczIRgcWmYgFnsk2j6m10H4tKzcqZM0JJ_NigY29pFimxlL7_qXMB1PorFJdlAKvp5SgjSTwLrXjkr1AqWwbpzG2yZUNN3GE8GvmTeo4yweQbNCd-yO_Zpozx0J34wHBEMuaw-ZfCUk7mdKKsg-EcE4Zv0Xgl9wP2MpKPx0V8gLazxe6UQ9ShzNuruSOncpLYJN_oQ4aKf5ptOp1rsfDY2IK9frtmRTKOdQ-MEmSdjGL_88IQcvCs7jqVz53XKoXRlXB8tMIGOcg-ICer6yxe2itIQ\",\"e\":\"AQAB\",\"x5c\":[\"MIIDBTCCAe2gAwIBAgIQff8yrFO3CINPHUTT76tUsTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMTAyNDE3NDU1NloXDTI2MTAyNDE3NDU1NlowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMq979bhE6xX09e87zostNcPJzvtbeu3JomS2catiEg3bXvXcPiMQke5T1dsHs3MyEYHFpmIBZ7JNo+ptdB+LSs3KmTNCSfzYoGNvaRYpsZS+/6lzAdT6KxSXZQCr6eUoI0k8C6145K9QKlsG6cxtsmVDTdxhPBr5k3qOMsHkGzQnfsjv2aaM8dCd+MBwRDLmsPmXwlJO5nSirIPhHBOGb9F4J" <> ..., + body: %{ + "keys" => [ + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/{tenantid}/v2.0", + "kid" => "nOo3ZDrODXEK1jKWhXslHR_KXEg", + "kty" => "RSA", + "n" => + "oaLLT9hkcSj2tGfZsjbu7Xz1Krs0qEicXPmEsJKOBQHauZ_kRM1HdEkgOJbUznUspE6xOuOSXjlzErqBxXAu4SCvcvVOCYG2v9G3-uIrLF5dstD0sYHBo1VomtKxzF90Vslrkn6rNQgUGIWgvuQTxm1uRklYFPEcTIRw0LnYknzJ06GC9ljKR617wABVrZNkBuDgQKj37qcyxoaxIGdxEcmVFZXJyrxDgdXh9owRmZn6LIJlGjZ9m59emfuwnBnsIQG7DirJwe9SXrLXnexRQWqyzCdkYaOqkpKrsjuxUj2-MHX31FqsdpJJsOAvYXGOYBKJRjhGrGdONVrZdUdTBQ", + "use" => "sig", + "x5c" => [ + "MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R" + ], + "x5t" => "nOo3ZDrODXEK1jKWhXslHR_KXEg" + }, + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/{tenantid}/v2.0", + "kid" => "l3sQ-50cCH4xBVZLHTGwnSR7680", + "kty" => "RSA", + "n" => + "sfsXMXWuO-dniLaIELa3Pyqz9Y_rWff_AVrCAnFSdPHa8__Pmkbt_yq-6Z3u1o4gjRpKWnrjxIh8zDn1Z1RS26nkKcNg5xfWxR2K8CPbSbY8gMrp_4pZn7tgrEmoLMkwfgYaVC-4MiFEo1P2gd9mCdgIICaNeYkG1bIPTnaqquTM5KfT971MpuOVOdM1ysiejdcNDvEb7v284PYZkw2imwqiBY3FR0sVG7jgKUotFvhd7TR5WsA20GS_6ZIkUUlLUbG_rXWGl0YjZLS_Uf4q8Hbo7u-7MaFn8B69F6YaFdDlXm_A0SpedVFWQFGzMsp43_6vEzjfrFDJVAYkwb6xUQ", + "use" => "sig", + "x5c" => [ + "MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ" + ], + "x5t" => "l3sQ-50cCH4xBVZLHTGwnSR7680" + }, + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/{tenantid}/v2.0", + "kid" => "Mr5-AUibfBii7Nd1jBebaxboXW0", + "kty" => "RSA", + "n" => + "yr3v1uETrFfT17zvOiy01w8nO-1t67cmiZLZxq2ISDdte9dw-IxCR7lPV2wezczIRgcWmYgFnsk2j6m10H4tKzcqZM0JJ_NigY29pFimxlL7_qXMB1PorFJdlAKvp5SgjSTwLrXjkr1AqWwbpzG2yZUNN3GE8GvmTeo4yweQbNCd-yO_Zpozx0J34wHBEMuaw-ZfCUk7mdKKsg-EcE4Zv0Xgl9wP2MpKPx0V8gLazxe6UQ9ShzNuruSOncpLYJN_oQ4aKf5ptOp1rsfDY2IK9frtmRTKOdQ-MEmSdjGL_88IQcvCs7jqVz53XKoXRlXB8tMIGOcg-ICer6yxe2itIQ", + "use" => "sig", + "x5c" => [ + "MIIDBTCCAe2gAwIBAgIQff8yrFO3CINPHUTT76tUsTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMTAyNDE3NDU1NloXDTI2MTAyNDE3NDU1NlowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMq979bhE6xX09e87zostNcPJzvtbeu3JomS2catiEg3bXvXcPiMQke5T1dsHs3MyEYHFpmIBZ7JNo+ptdB+LSs3KmTNCSfzYoGNvaRYpsZS+/6lzAdT6KxSXZQCr6eUoI0k8C6145K9QKlsG6cxtsmVDTdxhPBr5k3qOMsHkGzQnfsjv2aaM8dCd+MBwRDLmsPmXwlJO5nSirIPhHBOGb9F4JfcD9jKSj8dFfIC2s8XulEPUoczbq7kjp3KS2CTf6EOGin+abTqda7Hw2NiCvX67ZkUyjnUPjBJknYxi//PCEHLwrO46lc+d1yqF0ZVwfLTCBjnIPiAnq+ssXtorSECAwEAAaMhMB8wHQYDVR0OBBYEFDiZG6s5d9RvorpqbVdS2/MD8ZKhMA0GCSqGSIb3DQEBCwUAA4IBAQAQAPuqqKj2AgfC9ayx+qUu0vjzKYdZ6T+3ssJDOGwB1cLMXMTUVgFwj8bsX1ahDUJdzKpWtNj7bno+Ug85IyU7k89U0Ygr55zWU5h4wnnRrCu9QKvudUPnbiXoVuHPwcK8w1fdXZQB5Qq/kKzhNGY57cG1bwj3R/aIdCp+BjgFppOKjJpK7FKS8G2v70eIiCLMapK9lLEeQOxIvzctTsXy9EZ7wtaIiYky4ZSituphToJUkakHaQ6evbn82lTg6WZz1tmSmYnPqRdAff7aiQ1Sw9HpuzlZY/piTVqvd6AfKZqyxu/FhENE0Odv/0hlHzI15jKQWL1Ljc0Nm3y1skut" + ], + "x5t" => "Mr5-AUibfBii7Nd1jBebaxboXW0" + }, + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/{tenantid}/v2.0", + "kid" => "jS1Xo1OWDj_52vbwGNgvQO2VzMc", + "kty" => "RSA", + "n" => + "spvQcXWqYrMcvcqQmfSMYnbUC8U03YctnXyLIBe148OzhBrgdAOmPfMfJi_tUW8L9svVGpk5qG6dN0n669cRHKqU52GnG0tlyYXmzFC1hzHVgQz9ehve4tlJ7uw936XIUOAOxx3X20zdpx7gm4zHx4j2ZBlXskAj6U3adpHQNuwUE6kmngJWR-deWlEigMpRsvUVQ2O5h0-RSq8Wr_x7ud3K6GTtrzARamz9uk2IXatKYdnj5Jrk2jLY6nWt-GtxlA_l9XwIrOl6Sqa_pOGIpS01JKdxKvpBC9VdS8oXB-7P5qLksmv7tq-SbbiOec0cvU7WP7vURv104V4FiI_qoQ", + "use" => "sig", + "x5c" => [ + "MIIDBTCCAe2gAwIBAgIQHsetP+i8i6VIAmjmfVGv6jANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIyMDEzMDIzMDYxNFoXDTI3MDEzMDIzMDYxNFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKb0HF1qmKzHL3KkJn0jGJ21AvFNN2HLZ18iyAXtePDs4Qa4HQDpj3zHyYv7VFvC/bL1RqZOahunTdJ+uvXERyqlOdhpxtLZcmF5sxQtYcx1YEM/Xob3uLZSe7sPd+lyFDgDscd19tM3ace4JuMx8eI9mQZV7JAI+lN2naR0DbsFBOpJp4CVkfnXlpRIoDKUbL1FUNjuYdPkUqvFq/8e7ndyuhk7a8wEWps/bpNiF2rSmHZ4+Sa5Noy2Op1rfhrcZQP5fV8CKzpekqmv6ThiKUtNSSncSr6QQvVXUvKFwfuz+ai5LJr+7avkm24jnnNHL1O1j+71Eb9dOFeBYiP6qECAwEAAaMhMB8wHQYDVR0OBBYEFGzVFjAbYpU/2en4ry4LMLUHJ3GjMA0GCSqGSIb3DQEBCwUAA4IBAQBU0YdNVfdByvpwsPfwNdD8m1PLeeCKmLHQnWRI5600yEHuoUvoAJd5dwe1ZU1bHHRRKWN7AktUzofP3yF61xtizhEbyPjHK1tnR+iPEviWxVvK37HtfEPzuh1Vqp08bqY15McYUtf77l2HXTpak+UWYRYJBi++2umIDKY5UMqU+LEZnvaXybLUKN3xG4iy2q1Ab8syGFaUP7J3nCtVrR7ip39BnvSTTZZNo/OC7fYXJ2X4sN1/2ZhR5EtnAgwi2RvlZl0aWPrczArUCxDBCbsKPL/Up/kID1ir1VO4LT09ryfv2nx3y6l0YvuL7ePz4nGYCWHcbMVcUrQUXquZ3XtI" + ], + "x5t" => "jS1Xo1OWDj_52vbwGNgvQO2VzMc" + }, + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/{tenantid}/v2.0", + "kid" => "2ZQpJ3UpbjAYXYGaXEJl8lV0TOI", + "kty" => "RSA", + "n" => + "wEMMJtj9yMQd8QS6Vnm538K5GN1Pr_I31_LUl9-OCYu-9_DrDvPGjViQK9kOiCjBfyqoAL-pBecn9-XXaS-C4xZTn1ZRw--GELabuo0u-U6r3TKj42xFDEP-_R5RpOGshoC95lrKiU5teuhn4fBM3XfR2GB0dVMcpzN3h4-0OMvBK__Zr9tkQCU_KzXTbNCjyA7ybtbr83NF9k3KjpTyOyY2S-qvFbY-AoqMhL9Rp8r2HBj_vrsr6RX6GeiSxxjbEzDFA2VIcSKbSHvbNBEeW2KjLXkz6QG2LjKz5XsYLp6kv_-k9lPQBy_V7Ci4ZkhAN-6j1S1Kcq58aLbp0wDNKQ", + "use" => "sig", + "x5c" => [ + "MIIDBTCCAe2gAwIBAgIQH4FlYNA+UJlF0G3vy9ZrhTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIyMDUyMjIwMDI0OVoXDTI3MDUyMjIwMDI0OVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMBDDCbY/cjEHfEEulZ5ud/CuRjdT6/yN9fy1JffjgmLvvfw6w7zxo1YkCvZDogowX8qqAC/qQXnJ/fl12kvguMWU59WUcPvhhC2m7qNLvlOq90yo+NsRQxD/v0eUaThrIaAveZayolObXroZ+HwTN130dhgdHVTHKczd4ePtDjLwSv/2a/bZEAlPys102zQo8gO8m7W6/NzRfZNyo6U8jsmNkvqrxW2PgKKjIS/UafK9hwY/767K+kV+hnokscY2xMwxQNlSHEim0h72zQRHltioy15M+kBti4ys+V7GC6epL//pPZT0Acv1ewouGZIQDfuo9UtSnKufGi26dMAzSkCAwEAAaMhMB8wHQYDVR0OBBYEFLFr+sjUQ+IdzGh3eaDkzue2qkTZMA0GCSqGSIb3DQEBCwUAA4IBAQCiVN2A6ErzBinGYafC7vFv5u1QD6nbvY32A8KycJwKWy1sa83CbLFbFi92SGkKyPZqMzVyQcF5aaRZpkPGqjhzM+iEfsR2RIf+/noZBlR/esINfBhk4oBruj7SY+kPjYzV03NeY0cfO4JEf6kXpCqRCgp9VDRM44GD8mUV/ooN+XZVFIWs5Gai8FGZX9H8ZSgkIKbxMbVOhisMqNhhp5U3fT7VPsl94rilJ8gKXP/KBbpldrfmOAdVDgUC+MHw3sSXSt+VnorB4DU4mUQLcMriQmbXdQc8d1HUZYZEkcKaSgbygHLtByOJF44XUsBotsTfZ4i/zVjnYcjgUQmwmAWD" + ], + "x5t" => "2ZQpJ3UpbjAYXYGaXEJl8lV0TOI" + }, + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/{tenantid}/v2.0", + "kid" => "-KI3Q9nNR7bRofxmeZoXqbHZGew", + "kty" => "RSA", + "n" => + "tJL6Wr2JUsxLyNezPQh1J6zn6wSoDAhgRYSDkaMuEHy75VikiB8wg25WuR96gdMpookdlRvh7SnRvtjQN9b5m4zJCMpSRcJ5DuXl4mcd7Cg3Zp1C5-JmMq8J7m7OS9HpUQbA1yhtCHqP7XA4UnQI28J-TnGiAa3viPLlq0663Cq6hQw7jYo5yNjdJcV5-FS-xNV7UHR4zAMRruMUHxte1IZJzbJmxjKoEjJwDTtcd6DkI3yrkmYt8GdQmu0YBHTJSZiz-M10CY3LbvLzf-tbBNKQ_gfnGGKF7MvRCmPA_YF_APynrIG7p4vPDRXhpG3_CIt317NyvGoIwiv0At83kQ", + "use" => "sig", + "x5c" => [ + "MIIDBTCCAe2gAwIBAgIQGQ6YG6NleJxJGDRAwAd/ZTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIyMTAwMjE4MDY0OVoXDTI3MTAwMjE4MDY0OVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALSS+lq9iVLMS8jXsz0IdSes5+sEqAwIYEWEg5GjLhB8u+VYpIgfMINuVrkfeoHTKaKJHZUb4e0p0b7Y0DfW+ZuMyQjKUkXCeQ7l5eJnHewoN2adQufiZjKvCe5uzkvR6VEGwNcobQh6j+1wOFJ0CNvCfk5xogGt74jy5atOutwquoUMO42KOcjY3SXFefhUvsTVe1B0eMwDEa7jFB8bXtSGSc2yZsYyqBIycA07XHeg5CN8q5JmLfBnUJrtGAR0yUmYs/jNdAmNy27y83/rWwTSkP4H5xhihezL0QpjwP2BfwD8p6yBu6eLzw0V4aRt/wiLd9ezcrxqCMIr9ALfN5ECAwEAAaMhMB8wHQYDVR0OBBYEFJcSH+6Eaqucndn9DDu7Pym7OA8rMA0GCSqGSIb3DQEBCwUAA4IBAQADKkY0PIyslgWGmRDKpp/5PqzzM9+TNDhXzk6pw8aESWoLPJo90RgTJVf8uIj3YSic89m4ftZdmGFXwHcFC91aFe3PiDgCiteDkeH8KrrpZSve1pcM4SNjxwwmIKlJdrbcaJfWRsSoGFjzbFgOecISiVaJ9ZWpb89/+BeAz1Zpmu8DSyY22dG/K6ZDx5qNFg8pehdOUYY24oMamd4J2u2lUgkCKGBZMQgBZFwk+q7H86B/byGuTDEizLjGPTY/sMms1FAX55xBydxrADAer/pKrOF1v7Dq9C1Z9QVcm5D9G4DcenyWUdMyK43NXbVQLPxLOng51KO9icp2j4U7pwHP" + ], + "x5t" => "-KI3Q9nNR7bRofxmeZoXqbHZGew" + }, + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/{tenantid}/v2.0", + "kid" => "DqUu8gf-nAgcyjP3-SuplNAXAnc", + "kty" => "RSA", + "n" => + "1n7-nWSLeuWQzBRlYSbS8RjvWvkQeD7QL9fOWaGXbW73VNGH0YipZisPClFv6GzwfWECTWQp19WFe_lASka5-KEWkQVzCbEMaaafOIs7hC61P5cGgw7dhuW4s7f6ZYGZEzQ4F5rHE-YNRbvD51qirPNzKHk3nji1wrh0YtbPPIf--NbI98bCwLLh9avedOmqESzWOGECEMXv8LSM-B9SKg_4QuBtyBwwIakTuqo84swTBM5w8PdhpWZZDtPgH87Wz-_WjWvk99AjXl7l8pWPQJiKNujt_ck3NDFpzaLEppodhUsID0ptRA008eCU6l8T-ux19wZmb_yBnHcV3pFWhQ", + "use" => "sig", + "x5c" => [ + "MIIC8TCCAdmgAwIBAgIQYVk/tJ1e4phISvVrAALNKTANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMjAxMjIxMDAwMDAwWhcNMjUxMjIxMDAwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWfv6dZIt65ZDMFGVhJtLxGO9a+RB4PtAv185ZoZdtbvdU0YfRiKlmKw8KUW/obPB9YQJNZCnX1YV7+UBKRrn4oRaRBXMJsQxppp84izuELrU/lwaDDt2G5bizt/plgZkTNDgXmscT5g1Fu8PnWqKs83MoeTeeOLXCuHRi1s88h/741sj3xsLAsuH1q9506aoRLNY4YQIQxe/wtIz4H1IqD/hC4G3IHDAhqRO6qjzizBMEznDw92GlZlkO0+AfztbP79aNa+T30CNeXuXylY9AmIo26O39yTc0MWnNosSmmh2FSwgPSm1EDTTx4JTqXxP67HX3BmZv/IGcdxXekVaFAgMBAAGjITAfMB0GA1UdDgQWBBQ2r//lgTPcKughDkzmCtRlw+P9SzANBgkqhkiG9w0BAQsFAAOCAQEAsFdRyczNWh/qpYvcIZbDvWYzlrmFZc6blcUzns9zf7sUWtQZrZPu5DbetV2Gr2r3qtMDKXCUaR+pqoy3I2zxTX3x8bTNhZD9YAgAFlTLNSydTaK5RHyB/5kr6B7ZJeNIk3PRVhRGt6ybCJSjV/VYVkLR5fdLP+5GhvBESobAR/d0ntriTzp7/tLMb/oXx7w5Hu1m3I8rpMocoXfH2SH1GLmMXj6Mx1dtwCDYM6bsb3fhWRz9O9OMR6QNiTnq8q9wn1QzBAnRcswYzT1LKKBPNFSasCvLYOCPOZCL+W8N8jqa9ZRYNYKWXzmiSptgBEM24t3m5FUWzWqoLu9pIcnkPQ==" + ], + "x5t" => "DqUu8gf-nAgcyjP3-SuplNAXAnc" + }, + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/{tenantid}/v2.0", + "kid" => "OzZ5Dbmcso9Qzt2ModGmihg30Bo", + "kty" => "RSA", + "n" => + "01re9a2BUTtNtdFzLNI-QEHW8XhDiDMDbGMkxHRIYXH41zBccsXwH9vMi0HuxXHpXOzwtUYKwl93ZR37tp6lpvwlU1HePNmZpJ9D-XAvU73x03YKoZEdaFB39VsVyLih3fuPv6DPE2qT-TNE3X5YdIWOGFrcMkcXLsjO-BCq4qcSdBH2lBgEQUuD6nqreLZsg-gPzSDhjVScIUZGiD8M2sKxADiIHo5KlaZIyu32t8JkavP9jM7ItSAjzig1W2yvVQzUQZA-xZqJo2jxB3g_fygdPUHK6UN-_cqkrfxn2-VWH1wMhlm90SpxTMD4HoYOViz1ggH8GCX2aBiX5OzQ6Q", + "use" => "sig", + "x5c" => [ + "MIIC8TCCAdmgAwIBAgIQQrXPXIlUE4JMTMkVj+02YTANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMjEwMzEwMDAwMDAwWhcNMjYwMzEwMDAwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTWt71rYFRO0210XMs0j5AQdbxeEOIMwNsYyTEdEhhcfjXMFxyxfAf28yLQe7Fcelc7PC1RgrCX3dlHfu2nqWm/CVTUd482Zmkn0P5cC9TvfHTdgqhkR1oUHf1WxXIuKHd+4+/oM8TapP5M0Tdflh0hY4YWtwyRxcuyM74EKripxJ0EfaUGARBS4Pqeqt4tmyD6A/NIOGNVJwhRkaIPwzawrEAOIgejkqVpkjK7fa3wmRq8/2Mzsi1ICPOKDVbbK9VDNRBkD7FmomjaPEHeD9/KB09QcrpQ379yqSt/Gfb5VYfXAyGWb3RKnFMwPgehg5WLPWCAfwYJfZoGJfk7NDpAgMBAAGjITAfMB0GA1UdDgQWBBTECjBRANDPLGrn1p7qtwswtBU7JzANBgkqhkiG9w0BAQsFAAOCAQEAq1Ib4ERvXG5kiVmhfLOpun2ElVOLY+XkvVlyVjq35rZmSIGxgfFc08QOQFVmrWQYrlss0LbboH0cIKiD6+rVoZTMWxGEicOcGNFzrkcG0ulu0cghKYID3GKDTftYKEPkvu2vQmueqa4t2tT3PlYF7Fi2dboR5Y96Ugs8zqNwdBMRm677N/tJBk53CsOf9NnBaxZ1EGArmEHHIb80vODStv35ueLrfMRtCF/HcgkGxy2U8kaCzYmmzHh4zYDkeCwM3Cq2bGkG+Efe9hFYfDHw13DzTR+h9pPqFFiAxnZ3ofT96NrZHdYjwbfmM8cw3ldg0xQzGcwZjtyYmwJ6sDdRvQ==" + ], + "x5t" => "OzZ5Dbmcso9Qzt2ModGmihg30Bo" + }, + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", + "kid" => "1LTMzakihiRla_8z2BEJVXeWMqo", + "kty" => "RSA", + "n" => + "3sKcJSD4cHwTY5jYm5lNEzqk3wON1CaARO5EoWIQt5u-X-ZnW61CiRZpWpfhKwRYU153td5R8p-AJDWT-NcEJ0MHU3KiuIEPmbgJpS7qkyURuHRucDM2lO4L4XfIlvizQrlyJnJcd09uLErZEO9PcvKiDHoois2B4fGj7CsAe5UZgExJvACDlsQSku2JUyDmZUZP2_u_gCuqNJM5o0hW7FKRI3MFoYCsqSEmHnnumuJ2jF0RHDRWQpodhlAR6uKLoiWHqHO3aG7scxYMj5cMzkpe1Kq_Dm5yyHkMCSJ_JaRhwymFfV_SWkqd3n-WVZT0ADLEq0RNi9tqZ43noUnO_w", + "use" => "sig", + "x5c" => [ + "MIIDYDCCAkigAwIBAgIJAIB4jVVJ3BeuMA0GCSqGSIb3DQEBCwUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTAeFw0xNjA0MDUxNDQzMzVaFw0yMTA0MDQxNDQzMzVaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN7CnCUg+HB8E2OY2JuZTRM6pN8DjdQmgETuRKFiELebvl/mZ1utQokWaVqX4SsEWFNed7XeUfKfgCQ1k/jXBCdDB1NyoriBD5m4CaUu6pMlEbh0bnAzNpTuC+F3yJb4s0K5ciZyXHdPbixK2RDvT3Lyogx6KIrNgeHxo+wrAHuVGYBMSbwAg5bEEpLtiVMg5mVGT9v7v4ArqjSTOaNIVuxSkSNzBaGArKkhJh557pridoxdERw0VkKaHYZQEerii6Ilh6hzt2hu7HMWDI+XDM5KXtSqvw5ucsh5DAkifyWkYcMphX1f0lpKnd5/llWU9AAyxKtETYvbameN56FJzv8CAwEAAaOBijCBhzAdBgNVHQ4EFgQU9IdLLpbC2S8Wn1MCXsdtFac9SRYwWQYDVR0jBFIwUIAU9IdLLpbC2S8Wn1MCXsdtFac9SRahLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAIB4jVVJ3BeuMAsGA1UdDwQEAwIBxjANBgkqhkiG9w0BAQsFAAOCAQEAXk0sQAib0PGqvwELTlflQEKS++vqpWYPW/2gCVCn5shbyP1J7z1nT8kE/ZDVdl3LvGgTMfdDHaRF5ie5NjkTHmVOKbbHaWpTwUFbYAFBJGnx+s/9XSdmNmW9GlUjdpd6lCZxsI6888r0ptBgKINRRrkwMlq3jD1U0kv4JlsIhafUIOqGi4+hIDXBlY0F/HJPfUU75N885/r4CCxKhmfh3PBM35XOch/NGC67fLjqLN+TIWLoxnvil9m3jRjqOA9u50JUeDGZABIYIMcAdLpI2lcfru4wXcYXuQul22nAR7yOyGKNOKULoOTE4t4AeGRqCogXSxZgaTgKSBhvhE+MGg==" + ], + "x5t" => "1LTMzakihiRla_8z2BEJVXeWMqo" + }, + %{ + "e" => "AQAB", + "issuer" => "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", + "kid" => "bW8ZcMjBCnJZS-ibX5UQDNStvx4", + "kty" => "RSA", + "n" => + "2a70SwgqIh8U-Shj_VJJGBheEVk2F4ygmMCRtKUAb1jMP6R1j5Mc5xaqhgzlWjckJI1lx4rha1oNLrdg8tJBxdm8V8xZohCOanJ52uAwoc6FFTY3VRLaUZSJ3zCXfuJwy4KvFHJUAuLhLj0hVeq-y10CmRJ1_MPTuNRJLdblSWcXyWYIikIRggQWS04M-QjR7571mX-Lu_eDs8xJVrnNFMVGRmFqf3EFD4QLNjW9JJj0m_prnTv41V_E8AA7MQZ12ip3u5aeOAQqGjVyzdHxvV9laxta6XWaM8QSTIu_Zav1-aDYExp99nCP4Hw0_Oom5vK5N88DB8VM0mouQi8a8Q", + "use" => "sig", + "x5c" => [ + "MIIDYDCCAkigAwIBAgIJAN2X7t+ckntxMA0GCSqGSIb3DQEBCwUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTAeFw0yMTAzMjkyMzM4MzNaFw0yNjAzMjgyMzM4MzNaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANmu9EsIKiIfFPkoY/1SSRgYXhFZNheMoJjAkbSlAG9YzD+kdY+THOcWqoYM5Vo3JCSNZceK4WtaDS63YPLSQcXZvFfMWaIQjmpyedrgMKHOhRU2N1US2lGUid8wl37icMuCrxRyVALi4S49IVXqvstdApkSdfzD07jUSS3W5UlnF8lmCIpCEYIEFktODPkI0e+e9Zl/i7v3g7PMSVa5zRTFRkZhan9xBQ+ECzY1vSSY9Jv6a507+NVfxPAAOzEGddoqd7uWnjgEKho1cs3R8b1fZWsbWul1mjPEEkyLv2Wr9fmg2BMaffZwj+B8NPzqJubyuTfPAwfFTNJqLkIvGvECAwEAAaOBijCBhzAdBgNVHQ4EFgQU57BsETnF8TctGU87R4N9YxmNWoIwWQYDVR0jBFIwUIAU57BsETnF8TctGU87R4N9YxmNWoKhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAN2X7t+ckntxMAsGA1UdDwQEAwIBxjANBgkqhkiG9w0BAQsFAAOCAQEAcsk+LGlTzSQdnh3mtCBMNCGZCiTYvFcqenwjDf1/c4U+Yi7fxYmAXm7wVLX+GVMxpLPpzMuVOXztGoPMUgWH59CFWhsMvZbIUKsd8xbEKmls1ZIgxRYdagcWTGeBET6XIoF6Ba57BhRCxFPslhIpg27/HnfHtTdGfjRpafNbBYvC/9PL/s2E9U4AklpUn2W19UiJLRFgXGPjYPLW0j1Od0qzHHJ84saclVwvuOrpp75Y+0Du5Z2OrjNF1W4dEWZMJmmOe73ejAnoiWJI25kQpkd4ooNasw3HIZEJZ6cKctmPJLdvx0tJ8bde4DivtWOeFIwcAkokH2jlHmAOipNETw==" + ], + "x5t" => "bW8ZcMjBCnJZS-ibX5UQDNStvx4" + } + ] + }, headers: [ {"Cache-Control", "max-age=86400, private"}, {"Content-Type", "application/json; charset=utf-8"}, @@ -9,18 +142,16 @@ {"Access-Control-Allow-Origin", "*"}, {"Access-Control-Allow-Methods", "GET, OPTIONS"}, {"P3P", "CP=\"DSP CUR OTPi IND OTRi ONL FIN\""}, - {"x-ms-request-id", "ec143cc1-4f0e-4963-ad77-2402a1912000"}, + {"x-ms-request-id", "66b44222-1ec7-41ae-ad80-b5ec809c1e00"}, {"x-ms-ests-server", "2.1.14059.13 - WUS2 ProdSlices"}, {"X-XSS-Protection", "0"}, {"Set-Cookie", - "fpc=ApJYMLn0O4NKmq2CR0m5TyM; expires=Mon, 12-Dec-2022 19:37:11 GMT; path=/; secure; HttpOnly; SameSite=None"}, + "fpc=AhXhm59ZPKxAuoDA0xJOTXw; expires=Mon, 12-Dec-2022 20:59:22 GMT; path=/; secure; HttpOnly; SameSite=None"}, {"Set-Cookie", - "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7WevrTXMM7fylmF8MD4TGGjPNwE2OY80sEGX5uGYrl3tD7knNeyForNZxSBE5yNLtiL-pgqiNIJI7OLB_bKoDbEGLyfmMywO4uV-D85zH0ozid5Vs6baZRFIaVnawAV489q1Roo_LZdPreHs0XchjCsGN6ihHRTUYyYvaiDQFymddcs3b7lhdt5sMHHMdC2V610ZYEZWJ4VcbCuzjnSW4PULqL21PgLYkfj4bhtrZwBGTFl0gAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None"}, - {"Set-Cookie", - "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly"}, - {"Set-Cookie", - "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"}, - {"Date", "Sat, 12 Nov 2022 19:37:11 GMT"}, + "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7WevrisuCv4ebeqYc-_POPk9YcVRhn9bd7CUqVj30-s0iSKalFe-d2lZ-f4-XK0n9ihY93a7m9SsGmsIMY7zD9ez66cNlKITs_ezKr4bRpFPqimcqQaHWPo_UKxjSYOQvsOH1N0F2vp-2Ht7cFowAp8vUIO0oCj1Fbps_-Di7qvstKBXVmkHe6uENMnH0BpbyPPa0DskUrFDPPwH1po1Hthps2l35xFERrP6mYU6aS4jwW98gAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None"}, + {"Set-Cookie", "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly"}, + {"Set-Cookie", "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"}, + {"Date", "Sat, 12 Nov 2022 20:59:21 GMT"}, {"Content-Length", "15922"} ], request_url: "https://login.microsoftonline.com/common/discovery/v2.0/keys", diff --git a/test/fixtures/http/google/discovery_document.exs b/test/fixtures/http/google/discovery_document.exs index 0a9a5b4..67be63f 100644 --- a/test/fixtures/http/google/discovery_document.exs +++ b/test/fixtures/http/google/discovery_document.exs @@ -1,14 +1,56 @@ %HTTPoison.Response{ status_code: 200, - body: "{\n \"issuer\": \"https://accounts.google.com\",\n \"authorization_endpoint\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n \"device_authorization_endpoint\": \"https://oauth2.googleapis.com/device/code\",\n \"token_endpoint\": \"https://oauth2.googleapis.com/token\",\n \"userinfo_endpoint\": \"https://openidconnect.googleapis.com/v1/userinfo\",\n \"revocation_endpoint\": \"https://oauth2.googleapis.com/revoke\",\n \"jwks_uri\": \"https://www.googleapis.com/oauth2/v3/certs\",\n \"response_types_supported\": [\n \"code\",\n \"token\",\n \"id_token\",\n \"code token\",\n \"code id_token\",\n \"token id_token\",\n \"code token id_token\",\n \"none\"\n ],\n \"subject_types_supported\": [\n \"public\"\n ],\n \"id_token_signing_alg_values_supported\": [\n \"RS256\"\n ],\n \"scopes_supported\": [\n \"openid\",\n \"email\",\n \"profile\"\n ],\n \"token_endpoint_auth_methods_supported\": [\n \"client_secret_post\",\n \"client_secret_basic\"\n ],\n \"claims_supported\": [\n \"aud\",\n \"email\",\n \"email_verified\",\n \"exp\",\n \"family_name\",\n \"given_name\",\n \"iat\",\n \"iss\",\n \"locale\",\n \"name\",\n \"picture\",\n \"sub\"\n ],\n \"code_challenge_methods_supported\": [\n \"plain\",\n \"S256\"\n ],\n \"grant_types_supported\": [\n \"authorization_code\",\n \"refresh_token\",\n \"urn:ietf:params:oauth:grant-type:device_code\",\n \"urn:ietf:params:oauth:grant-type:jwt-bearer\"\n ]\n}\n", + body: %{ + "authorization_endpoint" => "https://accounts.google.com/o/oauth2/v2/auth", + "claims_supported" => [ + "aud", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "iss", + "locale", + "name", + "picture", + "sub" + ], + "code_challenge_methods_supported" => ["plain", "S256"], + "device_authorization_endpoint" => "https://oauth2.googleapis.com/device/code", + "grant_types_supported" => [ + "authorization_code", + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code", + "urn:ietf:params:oauth:grant-type:jwt-bearer" + ], + "id_token_signing_alg_values_supported" => ["RS256"], + "issuer" => "https://accounts.google.com", + "jwks_uri" => "https://www.googleapis.com/oauth2/v3/certs", + "response_types_supported" => [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "token id_token", + "code token id_token", + "none" + ], + "revocation_endpoint" => "https://oauth2.googleapis.com/revoke", + "scopes_supported" => ["openid", "email", "profile"], + "subject_types_supported" => ["public"], + "token_endpoint" => "https://oauth2.googleapis.com/token", + "token_endpoint_auth_methods_supported" => ["client_secret_post", "client_secret_basic"], + "userinfo_endpoint" => "https://openidconnect.googleapis.com/v1/userinfo" + }, headers: [ {"Accept-Ranges", "bytes"}, {"Vary", "Accept-Encoding"}, {"Access-Control-Allow-Origin", "*"}, {"Content-Security-Policy-Report-Only", "require-trusted-types-for 'script'; report-uri https://csp.withgoogle.com/csp/federated-signon-mpm-access"}, - {"Cross-Origin-Opener-Policy", - "same-origin; report-to=\"federated-signon-mpm-access\""}, + {"Cross-Origin-Opener-Policy", "same-origin; report-to=\"federated-signon-mpm-access\""}, {"Report-To", "{\"group\":\"federated-signon-mpm-access\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/federated-signon-mpm-access\"}]}"}, {"Content-Length", "1280"}, diff --git a/test/fixtures/http/google/jwks.exs b/test/fixtures/http/google/jwks.exs index 194e45c..0542ad2 100644 --- a/test/fixtures/http/google/jwks.exs +++ b/test/fixtures/http/google/jwks.exs @@ -1,6 +1,27 @@ %HTTPoison.Response{ status_code: 200, - body: "{\n \"keys\": [\n {\n \"use\": \"sig\",\n \"kid\": \"f451345fad08101bfb345cf642a2da9267b9ebeb\",\n \"kty\": \"RSA\",\n \"alg\": \"RS256\",\n \"n\": \"ppFPAZUqIVqCf_SffT6xDCXu1R7aRoT6TNT5_Q8PKxkkqbOVysJPNwliF-486VeM8KNW8onFOv0GkP0lJ2ASrVgyMG1qmlGUlKug64dMQXPxSlVUCXCPN676W5IZTvT0tD2byM_29HZXnOifRg-d7PRRvIBLSUWe-fGb1-tP2w65SOW-W6LuOjGzLNPJFYQvHyUx_uXHOCfIoSb8kaMwx8bCWvKc76yT0DG1wcygGXKuFQHW-Sdi1j_6bF19lVu30DX-jhYsNMUnGUr6g2iycQ50pWMORZqvcHVOH1bbDrWuz0b564sK0ET2B3XDR37djNQ305PxiQZaBStm-hM8Aw\",\n \"e\": \"AQAB\"\n },\n {\n \"n\": \"z8PS6saDU3h5ZbQb3Lwl_Arwgu65ECMi79KUlzx4tqk8bgxtaaHcqyvWqVdsA9H6Q2ZtQhBZivqV4Jg0HoPHcEwv46SEziFQNR2LH86e-WIDI5pk2NKg_9cFMee9Mz7f_NSQJ3uyD1pu86bdUTYhCw57DbEVDOuubClNMUV456dWx7dx5W4kdcQe63vGg9LXQ-9PPz9AL-0ZKr8eQEHp4KRfRUfngjqjYBMTFuuo38l94KR99B04Z-FboGnqYLgNxctwZ9eXbCerb9bV5-Q9Gb3zoo0x1h90tFdgmC2ZU1xcIIjHmFqJ29mSDZHYAAYtMNAeWreK4gqWJunc9o0vpQ\",\n \"kty\": \"RSA\",\n \"alg\": \"RS256\",\n \"kid\": \"713fd68c966e29380981edc0164a2f6c06c5702a\",\n \"use\": \"sig\",\n \"e\": \"AQAB\"\n }\n ]\n}\n", + body: %{ + "keys" => [ + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "f451345fad08101bfb345cf642a2da9267b9ebeb", + "kty" => "RSA", + "n" => + "ppFPAZUqIVqCf_SffT6xDCXu1R7aRoT6TNT5_Q8PKxkkqbOVysJPNwliF-486VeM8KNW8onFOv0GkP0lJ2ASrVgyMG1qmlGUlKug64dMQXPxSlVUCXCPN676W5IZTvT0tD2byM_29HZXnOifRg-d7PRRvIBLSUWe-fGb1-tP2w65SOW-W6LuOjGzLNPJFYQvHyUx_uXHOCfIoSb8kaMwx8bCWvKc76yT0DG1wcygGXKuFQHW-Sdi1j_6bF19lVu30DX-jhYsNMUnGUr6g2iycQ50pWMORZqvcHVOH1bbDrWuz0b564sK0ET2B3XDR37djNQ305PxiQZaBStm-hM8Aw", + "use" => "sig" + }, + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "713fd68c966e29380981edc0164a2f6c06c5702a", + "kty" => "RSA", + "n" => + "z8PS6saDU3h5ZbQb3Lwl_Arwgu65ECMi79KUlzx4tqk8bgxtaaHcqyvWqVdsA9H6Q2ZtQhBZivqV4Jg0HoPHcEwv46SEziFQNR2LH86e-WIDI5pk2NKg_9cFMee9Mz7f_NSQJ3uyD1pu86bdUTYhCw57DbEVDOuubClNMUV456dWx7dx5W4kdcQe63vGg9LXQ-9PPz9AL-0ZKr8eQEHp4KRfRUfngjqjYBMTFuuo38l94KR99B04Z-FboGnqYLgNxctwZ9eXbCerb9bV5-Q9Gb3zoo0x1h90tFdgmC2ZU1xcIIjHmFqJ29mSDZHYAAYtMNAeWreK4gqWJunc9o0vpQ", + "use" => "sig" + } + ] + }, headers: [ {"Server", "scaffolding on HTTPServer2"}, {"X-XSS-Protection", "0"}, diff --git a/test/fixtures/http/keycloak/discovery_document.exs b/test/fixtures/http/keycloak/discovery_document.exs index 42bae97..b37977f 100644 --- a/test/fixtures/http/keycloak/discovery_document.exs +++ b/test/fixtures/http/keycloak/discovery_document.exs @@ -1,6 +1,280 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"issuer\":\"http://localhost:8080/realms/master\",\"authorization_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/auth\",\"token_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/token\",\"introspection_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/token/introspect\",\"userinfo_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/userinfo\",\"end_session_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/logout\",\"frontchannel_logout_session_supported\":true,\"frontchannel_logout_supported\":true,\"jwks_uri\":\"http://localhost:8080/realms/master/protocol/openid-connect/certs\",\"check_session_iframe\":\"http://localhost:8080/realms/master/protocol/openid-connect/login-status-iframe.html\",\"grant_types_supported\":[\"authorization_code\",\"implicit\",\"refresh_token\",\"password\",\"client_credentials\",\"urn:ietf:params:oauth:grant-type:device_code\",\"urn:openid:params:grant-type:ciba\"],\"acr_values_supported\":[\"0\",\"1\"],\"response_types_supported\":[\"code\",\"none\",\"id_token\",\"token\",\"id_token token\",\"code id_token\",\"code token\",\"code id_token token\"],\"subject_types_supported\":[\"public\",\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"id_token_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"id_token_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"userinfo_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\",\"none\"],\"userinfo_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"userinfo_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"request_object_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\",\"none\"],\"request_object_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"request_object_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\",\"query.jwt\",\"fragment.jwt\",\"form_post.jwt\",\"jwt\"],\"registration_endpoint\":\"http://localhost:8080/realms/master/clients-registrations/openid-connect\",\"token_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_secret_basic\",\"client_secret_post\",\"tls_client_auth\",\"client_secret_jwt\"],\"token_endpoint_auth_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"introspection_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_secret_basic\",\"client_secret_post\",\"tls_client_auth\",\"client_secret_jwt\"],\"introspection_endpoint_auth_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"authorization_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"authorization_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"authorization_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"claims_supported\":[\"aud\",\"sub\",\"iss\",\"auth_time\",\"name\",\"given_name\",\"family_name\",\"preferred_username\",\"email\",\"acr\"],\"claim_types_supported\":[\"normal\"],\"claims_parameter_supported\":true,\"scopes_supported\":[\"openid\",\"phone\",\"acr\",\"microprofile-jwt\",\"email\",\"profile\",\"web-origins\",\"offline_access\",\"address\",\"roles\"],\"request_parameter_supported\":true,\"request_uri_parameter_supported\":true,\"require_request_uri_registration\":true,\"code_challenge_methods_supported\":[\"plain\",\"S256\"],\"tls_client_certificate_bound_access_tokens\":true,\"revocation_endpoint\":\"http://localhost:8080/realms/master/protocol/openid-connect/revoke\",\"revocation_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_" <> ..., + body: %{ + "frontchannel_logout_supported" => true, + "userinfo_encryption_enc_values_supported" => [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "authorization_encryption_alg_values_supported" => ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"], + "scopes_supported" => [ + "openid", + "phone", + "acr", + "microprofile-jwt", + "email", + "profile", + "web-origins", + "offline_access", + "address", + "roles" + ], + "introspection_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/token/introspect", + "token_endpoint" => "http://localhost:8080/realms/master/protocol/openid-connect/token", + "backchannel_logout_session_supported" => true, + "token_endpoint_auth_methods_supported" => [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ], + "request_object_encryption_enc_values_supported" => [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "require_pushed_authorization_requests" => false, + "request_parameter_supported" => true, + "revocation_endpoint_auth_signing_alg_values_supported" => [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "device_authorization_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/auth/device", + "authorization_signing_alg_values_supported" => [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "code_challenge_methods_supported" => ["plain", "S256"], + "request_object_encryption_alg_values_supported" => ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"], + "id_token_signing_alg_values_supported" => [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "check_session_iframe" => + "http://localhost:8080/realms/master/protocol/openid-connect/login-status-iframe.html", + "issuer" => "http://localhost:8080/realms/master", + "id_token_encryption_alg_values_supported" => ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"], + "authorization_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/auth", + "userinfo_encryption_alg_values_supported" => ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"], + "id_token_encryption_enc_values_supported" => [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "backchannel_authentication_request_signing_alg_values_supported" => [ + "PS384", + "ES384", + "RS384", + "ES256", + "RS256", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "jwks_uri" => "http://localhost:8080/realms/master/protocol/openid-connect/certs", + "backchannel_authentication_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/ext/ciba/auth", + "subject_types_supported" => ["public", "pairwise"], + "authorization_encryption_enc_values_supported" => [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "userinfo_endpoint" => "http://localhost:8080/realms/master/protocol/openid-connect/userinfo", + "response_types_supported" => [ + "code", + "none", + "id_token", + "token", + "id_token token", + "code id_token", + "code token", + "code id_token token" + ], + "mtls_endpoint_aliases" => %{ + "backchannel_authentication_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/ext/ciba/auth", + "device_authorization_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/auth/device", + "introspection_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/token/introspect", + "pushed_authorization_request_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/ext/par/request", + "registration_endpoint" => + "http://localhost:8080/realms/master/clients-registrations/openid-connect", + "revocation_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/revoke", + "token_endpoint" => "http://localhost:8080/realms/master/protocol/openid-connect/token", + "userinfo_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/userinfo" + }, + "backchannel_token_delivery_modes_supported" => ["poll", "ping"], + "pushed_authorization_request_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/ext/par/request", + "grant_types_supported" => [ + "authorization_code", + "implicit", + "refresh_token", + "password", + "client_credentials", + "urn:ietf:params:oauth:grant-type:device_code", + "urn:openid:params:grant-type:ciba" + ], + "request_object_signing_alg_values_supported" => [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512", + "none" + ], + "tls_client_certificate_bound_access_tokens" => true, + "userinfo_signing_alg_values_supported" => [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512", + "none" + ], + "claims_supported" => [ + "aud", + "sub", + "iss", + "auth_time", + "name", + "given_name", + "family_name", + "preferred_username", + "email", + "acr" + ], + "introspection_endpoint_auth_signing_alg_values_supported" => [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "revocation_endpoint_auth_methods_supported" => [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "acr_values_supported" => ["0", "1"], + "registration_endpoint" => + "http://localhost:8080/realms/master/clients-registrations/openid-connect", + "frontchannel_logout_session_supported" => true, + "require_request_uri_registration" => true, + "revocation_endpoint" => "http://localhost:8080/realms/master/protocol/openid-connect/revoke", + "request_uri_parameter_supported" => true, + "end_session_endpoint" => + "http://localhost:8080/realms/master/protocol/openid-connect/logout", + "claims_parameter_supported" => true, + "response_modes_supported" => [ + "query", + "fragment", + "form_post", + "query.jwt", + "fragment.jwt", + "form_post.jwt", + "jwt" + ], + "claim_types_supported" => ["normal"], + "backchannel_logout_supported" => true, + "introspection_endpoint_auth_methods_supported" => [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ] + }, headers: [ {"Referrer-Policy", "no-referrer"}, {"X-Frame-Options", "SAMEORIGIN"}, diff --git a/test/fixtures/http/keycloak/jwks.exs b/test/fixtures/http/keycloak/jwks.exs index e3cd47f..8cd16aa 100644 --- a/test/fixtures/http/keycloak/jwks.exs +++ b/test/fixtures/http/keycloak/jwks.exs @@ -1,6 +1,37 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"keys\":[{\"kid\":\"nB0vgzwAcgJmjXwQZSzBMNkhoCaH4fvSwx5GC8Jq93c\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"use\":\"sig\",\"n\":\"zTxXhjNpLJy13O1sqVqZnlbqB0U618c9micjrs2f4NzdPT7rwLRnG3TYgTgMLDN8ERLffw-5RLZAOvTyryC0JLL2KhM-n6myrpJ5vjemp-l4f-RpcgtEVx1pe8ylKpj3SytZglMBC8ds8zFHMMf3y2HrqRUPfPHKCmdpRkLs7PhEBv8A3OTgCtp-g1YUB4s46vRun5AutMDogEXUMHdgkwPjTPTTRoUOiSulb5enhKIYj3xxUsK3yTT3Y6KkHuiHUvEfgX4l7ZsEAk1U1nA_-u86QlONRu4XVloiOHEU18zoFn6-xhER1j6lX000zTPf2FSbPpL2GdTPCN7Bj9t7rQ\",\"e\":\"AQAB\",\"x5c\":[\"MIICmzCCAYMCBgGEZ9w6QTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjIxMTExMTgwMTM2WhcNMzIxMTExMTgwMzE2WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNPFeGM2ksnLXc7WypWpmeVuoHRTrXxz2aJyOuzZ/g3N09PuvAtGcbdNiBOAwsM3wREt9/D7lEtkA69PKvILQksvYqEz6fqbKuknm+N6an6Xh/5GlyC0RXHWl7zKUqmPdLK1mCUwELx2zzMUcwx/fLYeupFQ988coKZ2lGQuzs+EQG/wDc5OAK2n6DVhQHizjq9G6fkC60wOiARdQwd2CTA+NM9NNGhQ6JK6Vvl6eEohiPfHFSwrfJNPdjoqQe6IdS8R+BfiXtmwQCTVTWcD/67zpCU41G7hdWWiI4cRTXzOgWfr7GERHWPqVfTTTNM9/YVJs+kvYZ1M8I3sGP23utAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALXouAkGrzu+5EpJLWwfQdtEYuEwK9VNheDdR2WqQDNwduk71hZWr5olNyhJz6ogMPOcNhQ33B9MssYqKtINrgxeSrO5k2QAYRYRq4BEJ+3Y9Yif9KNYV7IBXcNz4Z6fiFUnmIPoOU7dCb1Lt8sFLaa5EL1TVKQkMJ5nXLpMtTHywwEPvb/Aul1ssydfanpDW4/nCf512UpsnWetfiKw+IlTB7Rt5zMQ65RAimxz+hnY2+LF/XGnyNnPY6IPvX9suyt6u3EBM2xdXD+2UedJ8EbSvPTVo9nPfH59HmeIrIyJcw6/4xEjEVWN8oQEeYM3VOACORr2yK7zqbVpHQvOcz4=\"],\"x5t\":\"JZ8jwBZn8nzU0Rl33DcHwZU_Qio\",\"x5t#S256\":\"Y1XJpsaLUg3zbJVHyJij-zkNWmTbnM0y0Er1No84uTE\"},{\"kid\":\"Q2O_f0Z7hVL1WUxKQbChyWK_FzQK-qRZh4Kg9mxxt_I\",\"kty\":\"RSA\",\"alg\":\"RSA-OAEP\",\"use\":\"enc\",\"n\":\"jY-UVOgGl1Io_aL8jS8__Y25tqsefFfjR-JIcd3fhMjHWfoomfPlz0YfUHC6UYF7dgQeQnFEQqjxonJLDh32EWKWuXvcEvfp5592tx6COsOku_jeypwNurj9iGkbx3bv8w7x-SVp5VLdCM0IXBASIVTdmOwVfMJChIrJbjsk_wgCyL2IzU4w5cb2NdodEf1cf5ROt25EhdVZhvFzcxsfHaqOvKPBtP1W3FXbuVAIkFuXxSZdAKOZHS00RX_YOFIcOr5USIof9lBF_fXo7UXY-gDz95MkwgnfbC6WnVk4v57fniytwCNwZO3Smt3WTDBhAeFp9d8Xn_sUhwoqcBw_9Q\",\"e\":\"AQAB\",\"x5c\":[\"MIICmzCCAYMCBgGEZ9w7jTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjIxMTExMTgwMTM3WhcNMzIxMTExMTgwMzE3WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCNj5RU6AaXUij9ovyNLz/9jbm2qx58V+NH4khx3d+EyMdZ+iiZ8+XPRh9QcLpRgXt2BB5CcURCqPGicksOHfYRYpa5e9wS9+nnn3a3HoI6w6S7+N7KnA26uP2IaRvHdu/zDvH5JWnlUt0IzQhcEBIhVN2Y7BV8wkKEisluOyT/CALIvYjNTjDlxvY12h0R/Vx/lE63bkSF1VmG8XNzGx8dqo68o8G0/VbcVdu5UAiQW5fFJl0Ao5kdLTRFf9g4Uhw6vlRIih/2UEX99ejtRdj6APP3kyTCCd9sLpadWTi/nt+eLK3AI3Bk7dKa3dZMMGEB4Wn13xef+xSHCipwHD/1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD0e9ia9dDegLJFjRdAtfMZXj+6nVfSPNSPgMp4FBa1lx98EP5B3cEapVDRoAr/q3W3sI/88mzzXhrjhZEENep02JcKTZdZfyRoPShyVcPTLLUH0iiRYszTYj05iUpB9/wETN7rAjqpP+CV2a5uUL14K4sPZeWKOx3wCjEl7AdzlWCc65/XB1ZRVrRF0zJPcKQWb0YWgJb5cbj6/PNR3ZCHUw+PYi+i3/lJ3XObXmv/5+2PP0eXmeo9eTxoctKN947He95ugsOekzB2nU1XNcxDZzlMvKD2OiwkuG9SM+Uw7/sTBf/X/pHfzF9sKeq7B0vtHDunm+uBvRTZfbrDYKWk=\"],\"x5t\":\"Y8cA4ZtbCbhVKJWC4swg2H3oBRE\",\"x5t#S256\":\"pYO4_DMcglAP1G5HUhZxwlTo_nnDTpt7ORinPpUEiRc\"}]}", + body: %{ + "keys" => [ + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "nB0vgzwAcgJmjXwQZSzBMNkhoCaH4fvSwx5GC8Jq93c", + "kty" => "RSA", + "n" => + "zTxXhjNpLJy13O1sqVqZnlbqB0U618c9micjrs2f4NzdPT7rwLRnG3TYgTgMLDN8ERLffw-5RLZAOvTyryC0JLL2KhM-n6myrpJ5vjemp-l4f-RpcgtEVx1pe8ylKpj3SytZglMBC8ds8zFHMMf3y2HrqRUPfPHKCmdpRkLs7PhEBv8A3OTgCtp-g1YUB4s46vRun5AutMDogEXUMHdgkwPjTPTTRoUOiSulb5enhKIYj3xxUsK3yTT3Y6KkHuiHUvEfgX4l7ZsEAk1U1nA_-u86QlONRu4XVloiOHEU18zoFn6-xhER1j6lX000zTPf2FSbPpL2GdTPCN7Bj9t7rQ", + "use" => "sig", + "x5c" => [ + "MIICmzCCAYMCBgGEZ9w6QTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjIxMTExMTgwMTM2WhcNMzIxMTExMTgwMzE2WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNPFeGM2ksnLXc7WypWpmeVuoHRTrXxz2aJyOuzZ/g3N09PuvAtGcbdNiBOAwsM3wREt9/D7lEtkA69PKvILQksvYqEz6fqbKuknm+N6an6Xh/5GlyC0RXHWl7zKUqmPdLK1mCUwELx2zzMUcwx/fLYeupFQ988coKZ2lGQuzs+EQG/wDc5OAK2n6DVhQHizjq9G6fkC60wOiARdQwd2CTA+NM9NNGhQ6JK6Vvl6eEohiPfHFSwrfJNPdjoqQe6IdS8R+BfiXtmwQCTVTWcD/67zpCU41G7hdWWiI4cRTXzOgWfr7GERHWPqVfTTTNM9/YVJs+kvYZ1M8I3sGP23utAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALXouAkGrzu+5EpJLWwfQdtEYuEwK9VNheDdR2WqQDNwduk71hZWr5olNyhJz6ogMPOcNhQ33B9MssYqKtINrgxeSrO5k2QAYRYRq4BEJ+3Y9Yif9KNYV7IBXcNz4Z6fiFUnmIPoOU7dCb1Lt8sFLaa5EL1TVKQkMJ5nXLpMtTHywwEPvb/Aul1ssydfanpDW4/nCf512UpsnWetfiKw+IlTB7Rt5zMQ65RAimxz+hnY2+LF/XGnyNnPY6IPvX9suyt6u3EBM2xdXD+2UedJ8EbSvPTVo9nPfH59HmeIrIyJcw6/4xEjEVWN8oQEeYM3VOACORr2yK7zqbVpHQvOcz4=" + ], + "x5t" => "JZ8jwBZn8nzU0Rl33DcHwZU_Qio", + "x5t#S256" => "Y1XJpsaLUg3zbJVHyJij-zkNWmTbnM0y0Er1No84uTE" + }, + %{ + "alg" => "RSA-OAEP", + "e" => "AQAB", + "kid" => "Q2O_f0Z7hVL1WUxKQbChyWK_FzQK-qRZh4Kg9mxxt_I", + "kty" => "RSA", + "n" => + "jY-UVOgGl1Io_aL8jS8__Y25tqsefFfjR-JIcd3fhMjHWfoomfPlz0YfUHC6UYF7dgQeQnFEQqjxonJLDh32EWKWuXvcEvfp5592tx6COsOku_jeypwNurj9iGkbx3bv8w7x-SVp5VLdCM0IXBASIVTdmOwVfMJChIrJbjsk_wgCyL2IzU4w5cb2NdodEf1cf5ROt25EhdVZhvFzcxsfHaqOvKPBtP1W3FXbuVAIkFuXxSZdAKOZHS00RX_YOFIcOr5USIof9lBF_fXo7UXY-gDz95MkwgnfbC6WnVk4v57fniytwCNwZO3Smt3WTDBhAeFp9d8Xn_sUhwoqcBw_9Q", + "use" => "enc", + "x5c" => [ + "MIICmzCCAYMCBgGEZ9w7jTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjIxMTExMTgwMTM3WhcNMzIxMTExMTgwMzE3WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCNj5RU6AaXUij9ovyNLz/9jbm2qx58V+NH4khx3d+EyMdZ+iiZ8+XPRh9QcLpRgXt2BB5CcURCqPGicksOHfYRYpa5e9wS9+nnn3a3HoI6w6S7+N7KnA26uP2IaRvHdu/zDvH5JWnlUt0IzQhcEBIhVN2Y7BV8wkKEisluOyT/CALIvYjNTjDlxvY12h0R/Vx/lE63bkSF1VmG8XNzGx8dqo68o8G0/VbcVdu5UAiQW5fFJl0Ao5kdLTRFf9g4Uhw6vlRIih/2UEX99ejtRdj6APP3kyTCCd9sLpadWTi/nt+eLK3AI3Bk7dKa3dZMMGEB4Wn13xef+xSHCipwHD/1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD0e9ia9dDegLJFjRdAtfMZXj+6nVfSPNSPgMp4FBa1lx98EP5B3cEapVDRoAr/q3W3sI/88mzzXhrjhZEENep02JcKTZdZfyRoPShyVcPTLLUH0iiRYszTYj05iUpB9/wETN7rAjqpP+CV2a5uUL14K4sPZeWKOx3wCjEl7AdzlWCc65/XB1ZRVrRF0zJPcKQWb0YWgJb5cbj6/PNR3ZCHUw+PYi+i3/lJ3XObXmv/5+2PP0eXmeo9eTxoctKN947He95ugsOekzB2nU1XNcxDZzlMvKD2OiwkuG9SM+Uw7/sTBf/X/pHfzF9sKeq7B0vtHDunm+uBvRTZfbrDYKWk=" + ], + "x5t" => "Y8cA4ZtbCbhVKJWC4swg2H3oBRE", + "x5t#S256" => "pYO4_DMcglAP1G5HUhZxwlTo_nnDTpt7ORinPpUEiRc" + } + ] + }, headers: [ {"Referrer-Policy", "no-referrer"}, {"X-Frame-Options", "SAMEORIGIN"}, diff --git a/test/fixtures/http/okta/discovery_document.exs b/test/fixtures/http/okta/discovery_document.exs index 7e646ab..c177a0f 100644 --- a/test/fixtures/http/okta/discovery_document.exs +++ b/test/fixtures/http/okta/discovery_document.exs @@ -1,6 +1,111 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"issuer\":\"https://common.okta.com\",\"authorization_endpoint\":\"https://common.okta.com/oauth2/v1/authorize\",\"token_endpoint\":\"https://common.okta.com/oauth2/v1/token\",\"userinfo_endpoint\":\"https://common.okta.com/oauth2/v1/userinfo\",\"registration_endpoint\":\"https://common.okta.com/oauth2/v1/clients\",\"jwks_uri\":\"https://common.okta.com/oauth2/v1/keys\",\"response_types_supported\":[\"code\",\"id_token\",\"code id_token\",\"code token\",\"id_token token\",\"code id_token token\"],\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\",\"okta_post_message\"],\"grant_types_supported\":[\"authorization_code\",\"implicit\",\"refresh_token\",\"password\",\"urn:ietf:params:oauth:grant-type:device_code\"],\"subject_types_supported\":[\"public\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"scopes_supported\":[\"openid\",\"email\",\"profile\",\"address\",\"phone\",\"offline_access\",\"groups\"],\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"none\"],\"claims_supported\":[\"iss\",\"ver\",\"sub\",\"aud\",\"iat\",\"exp\",\"jti\",\"auth_time\",\"amr\",\"idp\",\"nonce\",\"name\",\"nickname\",\"preferred_username\",\"given_name\",\"middle_name\",\"family_name\",\"email\",\"email_verified\",\"profile\",\"zoneinfo\",\"locale\",\"address\",\"phone_number\",\"picture\",\"website\",\"gender\",\"birthdate\",\"updated_at\",\"at_hash\",\"c_hash\"],\"code_challenge_methods_supported\":[\"S256\"],\"introspection_endpoint\":\"https://common.okta.com/oauth2/v1/introspect\",\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"none\"],\"revocation_endpoint\":\"https://common.okta.com/oauth2/v1/revoke\",\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"none\"],\"end_session_endpoint\":\"https://common.okta.com/oauth2/v1/logout\",\"request_parameter_supported\":true,\"request_object_signing_alg_values_supported\":[\"HS256\",\"HS384\",\"HS512\",\"RS256\",\"RS384\",\"RS512\",\"ES256\",\"ES384\",\"ES512\"],\"device_authorization_endpoint\":\"https://common.okta.com/oauth2/v1/device/authorize\"}", + body: %{ + "authorization_endpoint" => "https://common.okta.com/oauth2/v1/authorize", + "claims_supported" => [ + "iss", + "ver", + "sub", + "aud", + "iat", + "exp", + "jti", + "auth_time", + "amr", + "idp", + "nonce", + "name", + "nickname", + "preferred_username", + "given_name", + "middle_name", + "family_name", + "email", + "email_verified", + "profile", + "zoneinfo", + "locale", + "address", + "phone_number", + "picture", + "website", + "gender", + "birthdate", + "updated_at", + "at_hash", + "c_hash" + ], + "code_challenge_methods_supported" => ["S256"], + "device_authorization_endpoint" => "https://common.okta.com/oauth2/v1/device/authorize", + "end_session_endpoint" => "https://common.okta.com/oauth2/v1/logout", + "grant_types_supported" => [ + "authorization_code", + "implicit", + "refresh_token", + "password", + "urn:ietf:params:oauth:grant-type:device_code" + ], + "id_token_signing_alg_values_supported" => ["RS256"], + "introspection_endpoint" => "https://common.okta.com/oauth2/v1/introspect", + "introspection_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none" + ], + "issuer" => "https://common.okta.com", + "jwks_uri" => "https://common.okta.com/oauth2/v1/keys", + "registration_endpoint" => "https://common.okta.com/oauth2/v1/clients", + "request_object_signing_alg_values_supported" => [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512" + ], + "request_parameter_supported" => true, + "response_modes_supported" => ["query", "fragment", "form_post", "okta_post_message"], + "response_types_supported" => [ + "code", + "id_token", + "code id_token", + "code token", + "id_token token", + "code id_token token" + ], + "revocation_endpoint" => "https://common.okta.com/oauth2/v1/revoke", + "revocation_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none" + ], + "scopes_supported" => [ + "openid", + "email", + "profile", + "address", + "phone", + "offline_access", + "groups" + ], + "subject_types_supported" => ["public"], + "token_endpoint" => "https://common.okta.com/oauth2/v1/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "none" + ], + "userinfo_endpoint" => "https://common.okta.com/oauth2/v1/userinfo" + }, headers: [ {"Date", "Sat, 12 Nov 2022 19:40:24 GMT"}, {"Content-Type", "application/json"}, diff --git a/test/fixtures/http/okta/jwks.exs b/test/fixtures/http/okta/jwks.exs index 7798981..b0cade4 100644 --- a/test/fixtures/http/okta/jwks.exs +++ b/test/fixtures/http/okta/jwks.exs @@ -1,6 +1,27 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"keys\":[{\"kty\":\"RSA\",\"alg\":\"RS256\",\"kid\":\"sljES1Bh0VMGwUpPfFunCVAzvLRdea7WluQfeV6zWTQ\",\"use\":\"sig\",\"e\":\"AQAB\",\"n\":\"7d7C4UL4HujzZesiEOtQUZcusHzBoUJVJXarHz0x9vMzQ1PYaGwivWJimnBHQXw6r1T05PQxOik9NnxvtPF7snPxVzDtDgrqzjd3WoYWmFiWrJz1vwebiioeFQKla7GkxfoE4cNFlIzi-i9y76zWwR3R3u0hUzHyY5XZcIBWnnInYKFACCNES7lqKu4qE3XTluJiP-WvDo79iFM67V2ZDowOWPLKoJQI0CA9l1Nkklaq32bjtMD9njl1Pl1KOKqZNyn1RzkmG0V15CYR959EEU7_Pl1LrrxGcgS-wafoyKILaJxEyeMWd3_SM0_anSAVvyUA46PYefcdEuURp-r4vQ\"},{\"kty\":\"RSA\",\"alg\":\"RS256\",\"kid\":\"TgT2Cxt-D-sVMlgTm3On7DLee8ljXgYhdzkrPkTc4sY\",\"use\":\"sig\",\"e\":\"AQAB\",\"n\":\"3VQaNpbZ67tiVkNqLF4j5skeys9D0Vzfu8NpOE8ZRVBmzLXa-FZ65cm6IGObMHhyDEBT4MTD3DLTRufVaiUbGcvrx5qee9eV_U3AwxSkRBEuHi-4HvUGkbvvXJpaoIHrNONZ_qLnL-GQm-kWTr3BaaRQ8lmMQjh3G4aCzzsFCpMT2HEe1GwCWDGTS_tDGt7oyueOtaPYFP3YLW7n8GW0-nVdiFxXYU0F-l9BF95YgYSut18r6xKk4EfHY4VNC6Y-qbldyEJ0iGdUT5sa07d7q6ocwDRO6iB07j65v43-A-H5vcew9N1JvFXXiJZ4Qn2UhzAGgUm6-Exr6fOko0W3zw\"}]}", + body: %{ + "keys" => [ + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "sljES1Bh0VMGwUpPfFunCVAzvLRdea7WluQfeV6zWTQ", + "kty" => "RSA", + "n" => + "7d7C4UL4HujzZesiEOtQUZcusHzBoUJVJXarHz0x9vMzQ1PYaGwivWJimnBHQXw6r1T05PQxOik9NnxvtPF7snPxVzDtDgrqzjd3WoYWmFiWrJz1vwebiioeFQKla7GkxfoE4cNFlIzi-i9y76zWwR3R3u0hUzHyY5XZcIBWnnInYKFACCNES7lqKu4qE3XTluJiP-WvDo79iFM67V2ZDowOWPLKoJQI0CA9l1Nkklaq32bjtMD9njl1Pl1KOKqZNyn1RzkmG0V15CYR959EEU7_Pl1LrrxGcgS-wafoyKILaJxEyeMWd3_SM0_anSAVvyUA46PYefcdEuURp-r4vQ", + "use" => "sig" + }, + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "TgT2Cxt-D-sVMlgTm3On7DLee8ljXgYhdzkrPkTc4sY", + "kty" => "RSA", + "n" => + "3VQaNpbZ67tiVkNqLF4j5skeys9D0Vzfu8NpOE8ZRVBmzLXa-FZ65cm6IGObMHhyDEBT4MTD3DLTRufVaiUbGcvrx5qee9eV_U3AwxSkRBEuHi-4HvUGkbvvXJpaoIHrNONZ_qLnL-GQm-kWTr3BaaRQ8lmMQjh3G4aCzzsFCpMT2HEe1GwCWDGTS_tDGt7oyueOtaPYFP3YLW7n8GW0-nVdiFxXYU0F-l9BF95YgYSut18r6xKk4EfHY4VNC6Y-qbldyEJ0iGdUT5sa07d7q6ocwDRO6iB07j65v43-A-H5vcew9N1JvFXXiJZ4Qn2UhzAGgUm6-Exr6fOko0W3zw", + "use" => "sig" + } + ] + }, headers: [ {"Date", "Sat, 12 Nov 2022 19:41:02 GMT"}, {"Content-Type", "application/json"}, diff --git a/test/fixtures/http/onelogin/discovery_document.exs b/test/fixtures/http/onelogin/discovery_document.exs index a5f5776..90bd1c9 100644 --- a/test/fixtures/http/onelogin/discovery_document.exs +++ b/test/fixtures/http/onelogin/discovery_document.exs @@ -1,6 +1,68 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"acr_values_supported\":[\"onelogin:nist:level:1:re-auth\"],\"authorization_endpoint\":\"https://common.onelogin.com/oidc/2/auth\",\"claims_parameter_supported\":true,\"claims_supported\":[\"sub\",\"email\",\"preferred_username\",\"name\",\"updated_at\",\"given_name\",\"family_name\",\"locale\",\"groups\",\"email_verified\",\"params\",\"phone_number\",\"acr\",\"sid\",\"auth_time\",\"iss\"],\"grant_types_supported\":[\"authorization_code\",\"implicit\",\"refresh_token\",\"client_credentials\",\"password\"],\"id_token_signing_alg_values_supported\":[\"HS256\",\"RS256\",\"PS256\"],\"issuer\":\"https://common.onelogin.com/oidc/2\",\"jwks_uri\":\"https://common.onelogin.com/oidc/2/certs\",\"registration_endpoint\":\"https://common.onelogin.com/oidc/2/register\",\"request_parameter_supported\":false,\"request_uri_parameter_supported\":false,\"response_modes_supported\":[\"form_post\",\"fragment\",\"query\"],\"response_types_supported\":[\"code\",\"id_token token\",\"id_token\"],\"scopes_supported\":[\"openid\",\"name\",\"profile\",\"groups\",\"email\",\"params\",\"phone\"],\"subject_types_supported\":[\"public\"],\"token_endpoint\":\"https://common.onelogin.com/oidc/2/token\",\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"none\"],\"userinfo_endpoint\":\"https://common.onelogin.com/oidc/2/me\",\"userinfo_signing_alg_values_supported\":[\"HS256\",\"RS256\",\"PS256\"],\"code_challenge_methods_supported\":[\"S256\"],\"introspection_endpoint\":\"https://common.onelogin.com/oidc/2/token/introspection\",\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"none\"],\"revocation_endpoint\":\"https://common.onelogin.com/oidc/2/token/revocation\",\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"none\"],\"end_session_endpoint\":\"https://common.onelogin.com/oidc/2/logout\",\"claim_types_supported\":[\"normal\"]}", + body: %{ + "acr_values_supported" => ["onelogin:nist:level:1:re-auth"], + "authorization_endpoint" => "https://common.onelogin.com/oidc/2/auth", + "claim_types_supported" => ["normal"], + "claims_parameter_supported" => true, + "claims_supported" => [ + "sub", + "email", + "preferred_username", + "name", + "updated_at", + "given_name", + "family_name", + "locale", + "groups", + "email_verified", + "params", + "phone_number", + "acr", + "sid", + "auth_time", + "iss" + ], + "code_challenge_methods_supported" => ["S256"], + "end_session_endpoint" => "https://common.onelogin.com/oidc/2/logout", + "grant_types_supported" => [ + "authorization_code", + "implicit", + "refresh_token", + "client_credentials", + "password" + ], + "id_token_signing_alg_values_supported" => ["HS256", "RS256", "PS256"], + "introspection_endpoint" => "https://common.onelogin.com/oidc/2/token/introspection", + "introspection_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "client_secret_post", + "none" + ], + "issuer" => "https://common.onelogin.com/oidc/2", + "jwks_uri" => "https://common.onelogin.com/oidc/2/certs", + "registration_endpoint" => "https://common.onelogin.com/oidc/2/register", + "request_parameter_supported" => false, + "request_uri_parameter_supported" => false, + "response_modes_supported" => ["form_post", "fragment", "query"], + "response_types_supported" => ["code", "id_token token", "id_token"], + "revocation_endpoint" => "https://common.onelogin.com/oidc/2/token/revocation", + "revocation_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "client_secret_post", + "none" + ], + "scopes_supported" => ["openid", "name", "profile", "groups", "email", "params", "phone"], + "subject_types_supported" => ["public"], + "token_endpoint" => "https://common.onelogin.com/oidc/2/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "client_secret_post", + "none" + ], + "userinfo_endpoint" => "https://common.onelogin.com/oidc/2/me", + "userinfo_signing_alg_values_supported" => ["HS256", "RS256", "PS256"] + }, headers: [ {"Date", "Sat, 12 Nov 2022 19:41:55 GMT"}, {"Content-Type", "application/json; charset=utf-8"}, @@ -9,8 +71,7 @@ {"vary", "Origin"}, {"strict-transport-security", "max-age=63072000; includeSubDomains;"}, {"x-content-type-options", "nosniff"}, - {"set-cookie", - "ol_oidc_canary_115=false; path=/; domain=.onelogin.com; HttpOnly; Secure"}, + {"set-cookie", "ol_oidc_canary_115=false; path=/; domain=.onelogin.com; HttpOnly; Secure"}, {"cache-control", "private"} ], request_url: "https://common.onelogin.com/oidc/2/.well-known/openid-configuration", diff --git a/test/fixtures/http/onelogin/jwks.exs b/test/fixtures/http/onelogin/jwks.exs index 5c93417..7842e8b 100644 --- a/test/fixtures/http/onelogin/jwks.exs +++ b/test/fixtures/http/onelogin/jwks.exs @@ -1,6 +1,18 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"JRcO4nxs5jgc8YdN7I2hLO4V_ql1bdoiMXmcYgHm4Hs\",\"use\":\"sig\",\"e\":\"AQAB\",\"n\":\"z8fZszkUNh1y1iSI6ZCkrwoZx1ZcFuQEngI8G_9VPjJXupqbgXedsV0YqDzQzYmdXd_lLb_OYWdyAP1FV6d2d4PfVjw4rGLqgYN5hEPFYqDEusiKtXyeh38xl37Nb8LGTX1qdstZjcXRo2YQ64W4UyuMko_TGOCxRNJg1fAfxRt1yV_ZeFV_93BMNjubV2D7kvpzaStJmYJi8A6QHqaqHaQkxAvYhJVi9XDajD3vvUlTVyOjURAnuaByA749glGBio5N9AfFTnYbHbeBOK3VJi6EJZzsuj3-5P4GUTYnSfrScs_kblaoeqt4GkExJqMZXGJTfGnX2UbYAjGHSTAoQw\",\"status\":\"active\"}]}", + body: %{ + "keys" => [ + %{ + "e" => "AQAB", + "kid" => "JRcO4nxs5jgc8YdN7I2hLO4V_ql1bdoiMXmcYgHm4Hs", + "kty" => "RSA", + "n" => + "z8fZszkUNh1y1iSI6ZCkrwoZx1ZcFuQEngI8G_9VPjJXupqbgXedsV0YqDzQzYmdXd_lLb_OYWdyAP1FV6d2d4PfVjw4rGLqgYN5hEPFYqDEusiKtXyeh38xl37Nb8LGTX1qdstZjcXRo2YQ64W4UyuMko_TGOCxRNJg1fAfxRt1yV_ZeFV_93BMNjubV2D7kvpzaStJmYJi8A6QHqaqHaQkxAvYhJVi9XDajD3vvUlTVyOjURAnuaByA749glGBio5N9AfFTnYbHbeBOK3VJi6EJZzsuj3-5P4GUTYnSfrScs_kblaoeqt4GkExJqMZXGJTfGnX2UbYAjGHSTAoQw", + "status" => "active", + "use" => "sig" + } + ] + }, headers: [ {"Date", "Sat, 12 Nov 2022 19:42:26 GMT"}, {"Content-Type", "application/json; charset=utf-8"}, @@ -9,8 +21,7 @@ {"vary", "Origin"}, {"strict-transport-security", "max-age=63072000; includeSubDomains;"}, {"x-content-type-options", "nosniff"}, - {"set-cookie", - "ol_oidc_canary_115=true; path=/; domain=.onelogin.com; HttpOnly; Secure"}, + {"set-cookie", "ol_oidc_canary_115=true; path=/; domain=.onelogin.com; HttpOnly; Secure"}, {"cache-control", "private"} ], request_url: "https://common.onelogin.com/oidc/2/certs", diff --git a/test/fixtures/http/vault/discovery_document.exs b/test/fixtures/http/vault/discovery_document.exs index c2a2c84..b5e54bf 100644 --- a/test/fixtures/http/vault/discovery_document.exs +++ b/test/fixtures/http/vault/discovery_document.exs @@ -1,6 +1,34 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"issuer\":\"http://0.0.0.0:8200/v1/identity/oidc/provider/default\",\"jwks_uri\":\"http://0.0.0.0:8200/v1/identity/oidc/provider/default/.well-known/keys\",\"authorization_endpoint\":\"http://0.0.0.0:8200/ui/vault/identity/oidc/provider/default/authorize\",\"token_endpoint\":\"http://0.0.0.0:8200/v1/identity/oidc/provider/default/token\",\"userinfo_endpoint\":\"http://0.0.0.0:8200/v1/identity/oidc/provider/default/userinfo\",\"request_parameter_supported\":false,\"request_uri_parameter_supported\":false,\"id_token_signing_alg_values_supported\":[\"RS256\",\"RS384\",\"RS512\",\"ES256\",\"ES384\",\"ES512\",\"EdDSA\"],\"response_types_supported\":[\"code\"],\"scopes_supported\":[\"openid\"],\"claims_supported\":[],\"subject_types_supported\":[\"public\"],\"grant_types_supported\":[\"authorization_code\"],\"token_endpoint_auth_methods_supported\":[\"none\",\"client_secret_basic\",\"client_secret_post\"]}", + body: %{ + "authorization_endpoint" => + "http://0.0.0.0:8200/ui/vault/identity/oidc/provider/default/authorize", + "claims_supported" => [], + "grant_types_supported" => ["authorization_code"], + "id_token_signing_alg_values_supported" => [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "EdDSA" + ], + "issuer" => "http://0.0.0.0:8200/v1/identity/oidc/provider/default", + "jwks_uri" => "http://0.0.0.0:8200/v1/identity/oidc/provider/default/.well-known/keys", + "request_parameter_supported" => false, + "request_uri_parameter_supported" => false, + "response_types_supported" => ["code"], + "scopes_supported" => ["openid"], + "subject_types_supported" => ["public"], + "token_endpoint" => "http://0.0.0.0:8200/v1/identity/oidc/provider/default/token", + "token_endpoint_auth_methods_supported" => [ + "none", + "client_secret_basic", + "client_secret_post" + ], + "userinfo_endpoint" => "http://0.0.0.0:8200/v1/identity/oidc/provider/default/userinfo" + }, headers: [ {"Cache-Control", "max-age=3600"}, {"Content-Type", "application/json"}, @@ -8,10 +36,12 @@ {"Date", "Sat, 12 Nov 2022 19:50:54 GMT"}, {"Content-Length", "849"} ], - request_url: "http://127.0.0.1:8200/v1/identity/oidc/provider/default/.well-known/openid-configuration", + request_url: + "http://127.0.0.1:8200/v1/identity/oidc/provider/default/.well-known/openid-configuration", request: %HTTPoison.Request{ method: :get, - url: "http://127.0.0.1:8200/v1/identity/oidc/provider/default/.well-known/openid-configuration", + url: + "http://127.0.0.1:8200/v1/identity/oidc/provider/default/.well-known/openid-configuration", headers: [], body: "", params: %{}, diff --git a/test/fixtures/http/vault/jwks.exs b/test/fixtures/http/vault/jwks.exs index 13b9692..0edb01a 100644 --- a/test/fixtures/http/vault/jwks.exs +++ b/test/fixtures/http/vault/jwks.exs @@ -1,6 +1,27 @@ %HTTPoison.Response{ status_code: 200, - body: "{\"keys\":[{\"use\":\"sig\",\"kty\":\"RSA\",\"kid\":\"cfdb8380-cbe8-a9f6-fcd4-51abcec905ce\",\"alg\":\"RS256\",\"n\":\"93riTBOuVsRDQPoXK-mJSDxbKj_m2nBZH6k47wFAzo1qPkcQqz6pcJMPLAZgBMuKUjXi0BNPwn7FO0jzyLA2dnMrk1Mu3qBBKfC6gD0TPe5r4EaQvtCuHzVr8sHd6io1FIhG4d7VmYK7wAtIwGwBix_NS7qitZsi-B8JlkttDXa_HsllB_81OCdgRctyHt3Up5RE7hjMEOn8kQv_UCSIexZ3GGQ4adkDe9Ufq-pfQCaBWxhxr6Ekr_P2beb-YWuEBlJdTFRR6jcluei5LFbseLcg5cYCfACNH10GAlURskHXXbfNrC8-9I7fgTlcntSSnsd0qFN-P6FQBd4aSwF1_Q\",\"e\":\"AQAB\"},{\"use\":\"sig\",\"kty\":\"RSA\",\"kid\":\"5e18c268-79c7-b761-16fd-3b241fa33336\",\"alg\":\"RS256\",\"n\":\"0YkIT7NWG0cWdIpV8GqPpn8b7x7N6AbmT1ZTvc-ozzVa8O-x4hscV2wCl1_UYjWwVVYQAUqg6NeqWTk3MGHfzTAxcSQALV4wMJkG98fCcazQ3KZZCpMfnaZymW5mEZyDMaZYaqanIcAlU0s6zCw7A145ykoeGUyd6j2u1_BC1LoqSLWz-8U2iJFZH2nq7wm9lVmzb2Wwe65zvvj6rg_HajgkJ2OBOEgnErZtZoJxV0sVsP87aO-KhZk97XAu0Y8sLzh7HtMMWHV_QrNTEUvVrAFzMDgAGC9Jc6nVM3idS0mdl70SWXQE_IjCrMf5ZXOliw1G2AMPXJVjae99l8fZKw\",\"e\":\"AQAB\"}]}", + body: %{ + "keys" => [ + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "cfdb8380-cbe8-a9f6-fcd4-51abcec905ce", + "kty" => "RSA", + "n" => + "93riTBOuVsRDQPoXK-mJSDxbKj_m2nBZH6k47wFAzo1qPkcQqz6pcJMPLAZgBMuKUjXi0BNPwn7FO0jzyLA2dnMrk1Mu3qBBKfC6gD0TPe5r4EaQvtCuHzVr8sHd6io1FIhG4d7VmYK7wAtIwGwBix_NS7qitZsi-B8JlkttDXa_HsllB_81OCdgRctyHt3Up5RE7hjMEOn8kQv_UCSIexZ3GGQ4adkDe9Ufq-pfQCaBWxhxr6Ekr_P2beb-YWuEBlJdTFRR6jcluei5LFbseLcg5cYCfACNH10GAlURskHXXbfNrC8-9I7fgTlcntSSnsd0qFN-P6FQBd4aSwF1_Q", + "use" => "sig" + }, + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "5e18c268-79c7-b761-16fd-3b241fa33336", + "kty" => "RSA", + "n" => + "0YkIT7NWG0cWdIpV8GqPpn8b7x7N6AbmT1ZTvc-ozzVa8O-x4hscV2wCl1_UYjWwVVYQAUqg6NeqWTk3MGHfzTAxcSQALV4wMJkG98fCcazQ3KZZCpMfnaZymW5mEZyDMaZYaqanIcAlU0s6zCw7A145ykoeGUyd6j2u1_BC1LoqSLWz-8U2iJFZH2nq7wm9lVmzb2Wwe65zvvj6rg_HajgkJ2OBOEgnErZtZoJxV0sVsP87aO-KhZk97XAu0Y8sLzh7HtMMWHV_QrNTEUvVrAFzMDgAGC9Jc6nVM3idS0mdl70SWXQE_IjCrMf5ZXOliw1G2AMPXJVjae99l8fZKw", + "use" => "sig" + } + ] + }, headers: [ {"Cache-Control", "no-store"}, {"Content-Type", "application/json"}, diff --git a/test/openid_connect/worker_test.exs b/test/openid_connect/worker_test.exs index cc5f8aa..3ed3692 100644 --- a/test/openid_connect/worker_test.exs +++ b/test/openid_connect/worker_test.exs @@ -8,7 +8,7 @@ defmodule OpenIDConnect.WorkerTest do @google_document Fixtures.load(:google, :discovery_document) @google_certs Fixtures.load(:google, :jwks) - alias OpenIDConnect.{HTTPClientMock} + alias OpenIDConnect.HTTPClientMock test "starting with :ignore does nothing" do :ignore = OpenIDConnect.Worker.start_link(:ignore) @@ -93,9 +93,13 @@ defmodule OpenIDConnect.WorkerTest do defp mock_http_requests do HTTPClientMock - |> expect(:get, fn "https://accounts.google.com/.well-known/openid-configuration", _headers, _opts -> + |> expect(:get, fn "https://accounts.google.com/.well-known/openid-configuration", + _headers, + _opts -> @google_document end) - |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, _opts -> @google_certs end) + |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, _opts -> + @google_certs + end) end end diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index ea9642e..9b69025 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -68,7 +68,7 @@ defmodule OpenIDConnectTest do assert expected_document == discovery_document assert expected_jwk == jwk - assert remaining_lifetime == 21527 + assert remaining_lifetime == 21_527 end test "fails during open id configuration document with HTTPoison error" do @@ -447,8 +447,7 @@ defmodule OpenIDConnectTest do "fail", "fail" ] - |> Enum.map(fn header -> Base.encode64(header) end) - |> Enum.join(".") + |> Enum.map_join(".", fn header -> Base.encode64(header) end) result = OpenIDConnect.verify(:google, token) assert result == {:error, :verify, "token claims did not contain a JSON payload"} @@ -470,8 +469,7 @@ defmodule OpenIDConnectTest do "{}", "{}" ] - |> Enum.map(fn header -> Base.encode64(header) end) - |> Enum.join(".") + |> Enum.map_join(".", fn header -> Base.encode64(header) end) result = OpenIDConnect.verify(:google, token) assert result == {:error, :verify, "no `alg` found in token"} diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 9468cd0..41447b0 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -10,7 +10,13 @@ defmodule Fixtures do response = Code.eval_file("test/fixtures/http/#{provider}/#{type}.exs") |> elem(0) + |> serialize() {:ok, response} end + + defp serialize(%HTTPoison.Response{body: body} = response), + do: %{response | body: Jason.encode!(body)} + + defp serialize(response), do: response end diff --git a/test/support/jason_encoder.ex b/test/support/jason_encoder.ex index b8a9957..d29319b 100644 --- a/test/support/jason_encoder.ex +++ b/test/support/jason_encoder.ex @@ -1,4 +1,8 @@ defmodule JasonEncoder do + @moduledoc """ + Convenience module to pass to JOSE + """ + def encode(term) do Jason.encode!(term) end diff --git a/test/support/mock_worker.ex b/test/support/mock_worker.ex index 9b43617..dcb74e9 100644 --- a/test/support/mock_worker.ex +++ b/test/support/mock_worker.ex @@ -1,4 +1,7 @@ defmodule OpenIDConnect.MockWorker do + @moduledoc """ + Mock the Worker. + """ use GenServer @google_document Fixtures.load(:google, :discovery_document) From 40d374cf6d1ffc67835a4505a21a8535727beaab Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Sat, 12 Nov 2022 13:24:31 -0800 Subject: [PATCH 05/32] Update README with forked info --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dedffe2..aa003c1 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,27 @@ # OpenIDConnect -Client library for consuming and working with OpenID Connect Providers +Firezone's fork of [DockYard's client library](https://github.com/dockyard/openid_connect) for consuming and working with OpenID Connect Providers. -**[OpenIDConnect is built and maintained by DockYard, contact us for expert Elixir and Phoenix consulting](https://dockyard.com/phoenix-consulting)**. +**[OpenIDConnect is originally built and maintained by DockYard, contact us for expert Elixir and Phoenix consulting](https://dockyard.com/phoenix-consulting)**. + +**This fork is maintained by the Firezone team**. ## Installation -[Available in Hex](https://hex.pm/packages/openid_connect), the package can be installed as: +~~[Available in Hex](https://hex.pm/packages/openid_connect), the package can be installed as:~~ + +EDIT: This fork is *not* published to Hex (yet). Add `openid_connect` to your list of dependencies in `mix.exs`: ```elixir def deps do - [{:openid_connect, "~> 0.2.2"}] + [{:openid_connect, github: "firezone/openid_connect"}] end ``` +**Note**: This library is not published to Hex (yet). We recommend installing + ## Getting Started @@ -142,7 +148,7 @@ end * [Brian Cardarella](http://twitter.com/bcardarella) -[We are very thankful for the many contributors](https://github.com/dockyard/openid_connect/graphs/contributors) +[We are very thankful for the many contributors](https://github.com//openid_connect/graphs/contributors) ## Versioning ## From f12f623f94632ac09d8586271a831aa349bac0ba Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Sat, 12 Nov 2022 13:45:24 -0800 Subject: [PATCH 06/32] Update weird README test --- test/openid_connect_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index 9b69025..0865a0a 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -16,7 +16,7 @@ defmodule OpenIDConnectTest do app_version = "#{Application.spec(app, :vsn)}" readme = File.read!("README.md") - [_, readme_versions] = Regex.run(~r/{:#{app}, "(.+)"}/, readme) + [_, readme_versions] = Regex.run(~r/{:#{app}, github: "(.+)"}/, readme) assert Version.match?( app_version, From 9cc404f4a4cc6fe3f216db3d3de4da819a73553d Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Sat, 12 Nov 2022 13:47:54 -0800 Subject: [PATCH 07/32] Skip readme test --- test/openid_connect_test.exs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index 0865a0a..bd9062d 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -11,12 +11,14 @@ defmodule OpenIDConnectTest do alias OpenIDConnect.{HTTPClientMock, MockWorker} + # XXX: Unskip this test when we're back on Hex + @tag :skip test "README install version check" do app = :openid_connect app_version = "#{Application.spec(app, :vsn)}" readme = File.read!("README.md") - [_, readme_versions] = Regex.run(~r/{:#{app}, github: "(.+)"}/, readme) + [_, readme_versions] = Regex.run(~r/{:#{app}, "(.+)"}/, readme) assert Version.match?( app_version, From 356c6763604a766a5dfd64d24e482db2af156053 Mon Sep 17 00:00:00 2001 From: Jamil Date: Sun, 13 Nov 2022 10:43:55 -0800 Subject: [PATCH 08/32] Apply suggestions from code review --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa003c1..815e5fb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ def deps do end ``` -**Note**: This library is not published to Hex (yet). We recommend installing +**Note**: This library is not published to Hex (yet). Install from GitHub instead. ## Getting Started From ff739f9c8aacf2b19d5f26200f6b87b8b48aa33c Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Mon, 14 Nov 2022 22:04:50 -0800 Subject: [PATCH 09/32] nil end_session_uri --- lib/openid_connect.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/openid_connect.ex b/lib/openid_connect.ex index f4d7501..f014245 100644 --- a/lib/openid_connect.ex +++ b/lib/openid_connect.ex @@ -381,6 +381,8 @@ defmodule OpenIDConnect do end end + defp build_uri(nil, _params), do: nil + defp build_uri(uri, params) do query = URI.encode_query(params) From c368b3414f814b636f704596bed60cb75dfc72e5 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 30 Dec 2022 10:43:46 -0600 Subject: [PATCH 10/32] Allow provider names as strings --- config/test.exs | 20 +++++++------- lib/openid_connect/worker.ex | 20 +++++++------- test/openid_connect/worker_test.exs | 16 +++++------ test/openid_connect_test.exs | 42 ++++++++++++++--------------- test/support/mock_worker.ex | 10 +++---- 5 files changed, 56 insertions(+), 52 deletions(-) diff --git a/config/test.exs b/config/test.exs index e6448bb..24fbd7a 100644 --- a/config/test.exs +++ b/config/test.exs @@ -2,12 +2,14 @@ import Config config :openid_connect, :http_client, OpenIDConnect.HTTPClientMock -config :openid_connect, :providers, - google: [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", - client_id: "CLIENT_ID_1", - client_secret: "CLIENT_SECRET_1", - redirect_uri: "https://dev.example.com:4200/session", - scope: "openid email profile", - response_type: "code id_token token" - ] +config :openid_connect, :providers, [ + {"google", + [ + discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", + client_id: "CLIENT_ID_1", + client_secret: "CLIENT_SECRET_1", + redirect_uri: "https://dev.example.com:4200/session", + scope: "openid email profile", + response_type: "code id_token token" + ]} +] diff --git a/lib/openid_connect/worker.ex b/lib/openid_connect/worker.ex index 1b73484..50c7d8e 100644 --- a/lib/openid_connect/worker.ex +++ b/lib/openid_connect/worker.ex @@ -28,32 +28,34 @@ defmodule OpenIDConnect.Worker do end def handle_call({:discovery_document, provider}, _from, state) do - discovery_document = get_in(state, [provider, :documents, :discovery_document]) + provider = Map.fetch!(state, provider) + discovery_document = provider.documents.discovery_document {:reply, discovery_document, state} end def handle_call({:jwk, provider}, _from, state) do - jwk = get_in(state, [provider, :documents, :jwk]) + provider = Map.fetch!(state, provider) + jwk = provider.documents.jwk {:reply, jwk, state} end def handle_call({:config, provider}, _from, state) do - config = get_in(state, [provider, :config]) + provider = Map.fetch!(state, provider) + config = provider.config {:reply, config, state} end def handle_info({:update_documents, provider}, state) do - config = get_in(state, [provider, :config]) + provider = Map.fetch!(state, provider) + config = provider.config documents = update_documents(provider, config) - - state = put_in(state, [provider, :documents], documents) - + state = Map.put(state, provider, %{provider | documents: documents}) {:noreply, state} end defp update_documents(provider, config) do - {:ok, %{remaining_lifetime: remaining_lifetime}} = - {:ok, documents} = OpenIDConnect.update_documents(config) + {:ok, %{remaining_lifetime: remaining_lifetime} = documents} = + OpenIDConnect.update_documents(config) refresh_time = time_until_next_refresh(remaining_lifetime) diff --git a/test/openid_connect/worker_test.exs b/test/openid_connect/worker_test.exs index 3ed3692..87899e0 100644 --- a/test/openid_connect/worker_test.exs +++ b/test/openid_connect/worker_test.exs @@ -5,8 +5,8 @@ defmodule OpenIDConnect.WorkerTest do setup :set_mox_global setup :verify_on_exit! - @google_document Fixtures.load(:google, :discovery_document) - @google_certs Fixtures.load(:google, :jwks) + @google_document Fixtures.load("google", :discovery_document) + @google_certs Fixtures.load("google", :jwks) alias OpenIDConnect.HTTPClientMock @@ -37,8 +37,8 @@ defmodule OpenIDConnect.WorkerTest do |> Jason.decode!() |> JOSE.JWK.from() - assert expected_document == get_in(state, [:google, :documents, :discovery_document]) - assert expected_jwk == get_in(state, [:google, :documents, :jwk]) + assert expected_document == get_in(state, ["google", :documents, :discovery_document]) + assert expected_jwk == get_in(state, ["google", :documents, :jwk]) end test "worker can respond to a call for the config" do @@ -48,9 +48,9 @@ defmodule OpenIDConnect.WorkerTest do {:ok, pid} = start_supervised({OpenIDConnect.Worker, config}) - google_config = GenServer.call(pid, {:config, :google}) + google_config = GenServer.call(pid, {:config, "google"}) - assert get_in(config, [:google]) == google_config + assert List.keyfind(config, "google", 0) == {"google", google_config} end test "worker can respond to a call for a provider's discovery document" do @@ -60,7 +60,7 @@ defmodule OpenIDConnect.WorkerTest do {:ok, pid} = start_supervised({OpenIDConnect.Worker, config}) - discovery_document = GenServer.call(pid, {:discovery_document, :google}) + discovery_document = GenServer.call(pid, {:discovery_document, "google"}) expected_document = @google_document @@ -79,7 +79,7 @@ defmodule OpenIDConnect.WorkerTest do {:ok, pid} = start_supervised({OpenIDConnect.Worker, config}) - jwk = GenServer.call(pid, {:jwk, :google}) + jwk = GenServer.call(pid, {:jwk, "google"}) expected_jwk = @google_certs diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index bd9062d..5bebf85 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -6,8 +6,8 @@ defmodule OpenIDConnectTest do setup :verify_on_exit! setup :set_jose_json_lib - @google_document Fixtures.load(:google, :discovery_document) - @google_certs Fixtures.load(:google, :jwks) + @google_document Fixtures.load("google", :discovery_document) + @google_certs Fixtures.load("google", :jwks) alias OpenIDConnect.{HTTPClientMock, MockWorker} @@ -193,7 +193,7 @@ defmodule OpenIDConnectTest do expected = "https://accounts.google.com/o/oauth2/v2/auth?client_id=CLIENT_ID_1&redirect_uri=https%3A%2F%2Fdev.example.com%3A4200%2Fsession&response_type=code+id_token+token&scope=openid+email+profile" - assert OpenIDConnect.authorization_uri(:google) == expected + assert OpenIDConnect.authorization_uri("google") == expected after GenServer.stop(pid) end @@ -206,7 +206,7 @@ defmodule OpenIDConnectTest do expected = "https://accounts.google.com/o/oauth2/v2/auth?client_id=CLIENT_ID_1&redirect_uri=https%3A%2F%2Fdev.example.com%3A4200%2Fsession&response_type=code+id_token+token&scope=openid+email+profile&hd=dockyard.com" - assert OpenIDConnect.authorization_uri(:google, %{"hd" => "dockyard.com"}) == expected + assert OpenIDConnect.authorization_uri("google", %{"hd" => "dockyard.com"}) == expected after GenServer.stop(pid) end @@ -219,7 +219,7 @@ defmodule OpenIDConnectTest do expected = "https://accounts.google.com/o/oauth2/v2/auth?client_id=CLIENT_ID_1&redirect_uri=https%3A%2F%2Fdev.example.com%3A4200%2Fsession&response_type=code+id_token+token&scope=something+else" - assert OpenIDConnect.authorization_uri(:google, %{scope: "something else"}) == expected + assert OpenIDConnect.authorization_uri("google", %{scope: "something else"}) == expected after GenServer.stop(pid) end @@ -232,7 +232,7 @@ defmodule OpenIDConnectTest do expected = "https://accounts.google.com/o/oauth2/v2/auth?client_id=CLIENT_ID_1&redirect_uri=https%3A%2F%2Fdev.example.com%3A4200%2Fsession&response_type=code+id_token+token&scope=openid+email+profile" - assert OpenIDConnect.authorization_uri(:google, %{}, :other_openid_worker) == expected + assert OpenIDConnect.authorization_uri("google", %{}, :other_openid_worker) == expected after GenServer.stop(pid) end @@ -243,7 +243,7 @@ defmodule OpenIDConnectTest do test "when token fetch is successful" do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) - config = GenServer.call(:openid_connect, {:config, :google}) + config = GenServer.call(:openid_connect, {:config, "google"}) form_body = [ client_id: config[:client_id], @@ -261,7 +261,7 @@ defmodule OpenIDConnectTest do {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{})}} end) - {:ok, body} = OpenIDConnect.fetch_tokens(:google, %{code: "1234"}) + {:ok, body} = OpenIDConnect.fetch_tokens("google", %{code: "1234"}) assert body == %{} after @@ -272,7 +272,7 @@ defmodule OpenIDConnectTest do test "when token fetch is successful with a different GenServer name" do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :other_openid_connect) - config = GenServer.call(:other_openid_connect, {:config, :google}) + config = GenServer.call(:other_openid_connect, {:config, "google"}) form_body = [ client_id: config[:client_id], @@ -293,7 +293,7 @@ defmodule OpenIDConnectTest do {:ok, body} = OpenIDConnect.fetch_tokens( - :google, + "google", %{code: "1234", id_token: "abcd"}, :other_openid_connect ) @@ -307,7 +307,7 @@ defmodule OpenIDConnectTest do test "when params are overridden" do {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) - config = GenServer.call(:openid_connect, {:config, :google}) + config = GenServer.call(:openid_connect, {:config, "google"}) form_body = [ client_id: config[:client_id], @@ -324,7 +324,7 @@ defmodule OpenIDConnectTest do {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{})}} end) - {:ok, body} = OpenIDConnect.fetch_tokens(:google, %{grant_type: "refresh_token"}) + {:ok, body} = OpenIDConnect.fetch_tokens("google", %{grant_type: "refresh_token"}) assert body == %{} after @@ -345,7 +345,7 @@ defmodule OpenIDConnectTest do {:ok, http_error} end) - resp = OpenIDConnect.fetch_tokens(:google, %{code: "1234"}) + resp = OpenIDConnect.fetch_tokens("google", %{code: "1234"}) assert resp == {:error, :fetch_tokens, http_error} after @@ -366,7 +366,7 @@ defmodule OpenIDConnectTest do {:ok, http_error} end) - resp = OpenIDConnect.fetch_tokens(:google, %{code: "1234"}) + resp = OpenIDConnect.fetch_tokens("google", %{code: "1234"}) assert resp == {:error, :fetch_tokens, http_error} after @@ -391,7 +391,7 @@ defmodule OpenIDConnectTest do |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) |> JOSE.JWS.compact() - result = OpenIDConnect.verify(:google, token) + result = OpenIDConnect.verify("google", token) assert result == {:ok, claims} after GenServer.stop(pid) @@ -415,7 +415,7 @@ defmodule OpenIDConnectTest do |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) |> JOSE.JWS.compact() - result = OpenIDConnect.verify(:google, token) + result = OpenIDConnect.verify("google", token) assert result == {:ok, claims} after GenServer.stop(pid) @@ -429,7 +429,7 @@ defmodule OpenIDConnectTest do {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) - result = OpenIDConnect.verify(:google, "fail") + result = OpenIDConnect.verify("google", "fail") assert result == {:error, :verify, "invalid token format"} after GenServer.stop(pid) @@ -451,7 +451,7 @@ defmodule OpenIDConnectTest do ] |> Enum.map_join(".", fn header -> Base.encode64(header) end) - result = OpenIDConnect.verify(:google, token) + result = OpenIDConnect.verify("google", token) assert result == {:error, :verify, "token claims did not contain a JSON payload"} after GenServer.stop(pid) @@ -473,7 +473,7 @@ defmodule OpenIDConnectTest do ] |> Enum.map_join(".", fn header -> Base.encode64(header) end) - result = OpenIDConnect.verify(:google, token) + result = OpenIDConnect.verify("google", token) assert result == {:error, :verify, "no `alg` found in token"} after GenServer.stop(pid) @@ -496,7 +496,7 @@ defmodule OpenIDConnectTest do |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) |> JOSE.JWS.compact() - result = OpenIDConnect.verify(:google, token) + result = OpenIDConnect.verify("google", token) assert result == {:error, :verify, "verification failed"} after GenServer.stop(pid) @@ -518,7 +518,7 @@ defmodule OpenIDConnectTest do |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) |> JOSE.JWS.compact() - result = OpenIDConnect.verify(:google, token <> " :)") + result = OpenIDConnect.verify("google", token <> " :)") assert result == {:error, :verify, "verification error"} after GenServer.stop(pid) diff --git a/test/support/mock_worker.ex b/test/support/mock_worker.ex index dcb74e9..edf359d 100644 --- a/test/support/mock_worker.ex +++ b/test/support/mock_worker.ex @@ -17,9 +17,9 @@ defmodule OpenIDConnect.MockWorker do |> JOSE.JWK.from() def init(_) do - config = + {"google", config} = Application.get_env(:openid_connect, :providers) - |> Keyword.get(:google) + |> List.keyfind("google", 0) {:ok, %{ @@ -29,15 +29,15 @@ defmodule OpenIDConnect.MockWorker do }} end - def handle_call({:discovery_document, :google}, _from, state) do + def handle_call({:discovery_document, "google"}, _from, state) do {:reply, Map.get(state, :document), state} end - def handle_call({:jwk, :google}, _from, state) do + def handle_call({:jwk, "google"}, _from, state) do {:reply, Map.get(state, :jwk), state} end - def handle_call({:config, :google}, _from, state) do + def handle_call({:config, "google"}, _from, state) do {:reply, Map.get(state, :config), state} end From e443f9dfb9b15cfeb8b75bff4b7cf126bc966566 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 30 Dec 2022 10:53:08 -0600 Subject: [PATCH 11/32] Add tests to make sure atom keys are still supported --- config/test.exs | 9 +++++++++ test/openid_connect/worker_test.exs | 22 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/config/test.exs b/config/test.exs index 24fbd7a..b5cdc7e 100644 --- a/config/test.exs +++ b/config/test.exs @@ -4,6 +4,15 @@ config :openid_connect, :http_client, OpenIDConnect.HTTPClientMock config :openid_connect, :providers, [ {"google", + [ + discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", + client_id: "CLIENT_ID_1", + client_secret: "CLIENT_SECRET_1", + redirect_uri: "https://dev.example.com:4200/session", + scope: "openid email profile", + response_type: "code id_token token" + ]}, + {:google2, [ discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", client_id: "CLIENT_ID_1", diff --git a/test/openid_connect/worker_test.exs b/test/openid_connect/worker_test.exs index 87899e0..5db3d8f 100644 --- a/test/openid_connect/worker_test.exs +++ b/test/openid_connect/worker_test.exs @@ -41,7 +41,7 @@ defmodule OpenIDConnect.WorkerTest do assert expected_jwk == get_in(state, ["google", :documents, :jwk]) end - test "worker can respond to a call for the config" do + test "worker can respond to a call for the config using string name" do mock_http_requests() config = Application.get_env(:openid_connect, :providers) @@ -53,6 +53,18 @@ defmodule OpenIDConnect.WorkerTest do assert List.keyfind(config, "google", 0) == {"google", google_config} end + test "worker can respond to a call for the config using atom name" do + mock_http_requests() + + config = Application.get_env(:openid_connect, :providers) + + {:ok, pid} = start_supervised({OpenIDConnect.Worker, config}) + + google_config = GenServer.call(pid, {:config, :google2}) + + assert List.keyfind(config, :google2, 0) == {:google2, google_config} + end + test "worker can respond to a call for a provider's discovery document" do mock_http_requests() @@ -101,5 +113,13 @@ defmodule OpenIDConnect.WorkerTest do |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, _opts -> @google_certs end) + |> expect(:get, fn "https://accounts.google.com/.well-known/openid-configuration", + _headers, + _opts -> + @google_document + end) + |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, _opts -> + @google_certs + end) end end From dca9c7241cc0bb9f311c88e79c0b5cf747988e31 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Sun, 8 Jan 2023 12:58:28 -0600 Subject: [PATCH 12/32] Allow runtime configuration change without restart --- lib/openid_connect.ex | 4 ++++ lib/openid_connect/worker.ex | 19 ++++++++++++------- test/openid_connect/worker_test.exs | 4 ++-- test/openid_connect_test.exs | 21 +++++++++++++++++++++ test/support/mock_worker.ex | 23 +++++++++++++++-------- 5 files changed, 54 insertions(+), 17 deletions(-) diff --git a/lib/openid_connect.ex b/lib/openid_connect.ex index f014245..e0f7cbb 100644 --- a/lib/openid_connect.ex +++ b/lib/openid_connect.ex @@ -241,6 +241,10 @@ defmodule OpenIDConnect do end end + def reconfigure(provider_configs, name \\ :openid_connect) do + GenServer.cast(name, {:reconfigure, provider_configs}) + end + @doc false def normalize_discovery_document(discovery_document) do # claims_supported may be missing as it is marked RECOMMENDED by the spec, default to an empty list diff --git a/lib/openid_connect/worker.ex b/lib/openid_connect/worker.ex index 50c7d8e..4b3b24f 100644 --- a/lib/openid_connect/worker.ex +++ b/lib/openid_connect/worker.ex @@ -14,17 +14,18 @@ defmodule OpenIDConnect.Worker do end def init(:ignore) do - :ignore + {:ok, []} end def init(provider_configs) do - state = - Enum.into(provider_configs, %{}, fn {provider, config} -> - documents = update_documents(provider, config) - {provider, %{config: config, documents: documents}} - end) + {:ok, build_state(provider_configs)} + end - {:ok, state} + defp build_state(provider_configs) do + Enum.into(provider_configs, %{}, fn {provider, config} -> + documents = update_documents(provider, config) + {provider, %{config: config, documents: documents}} + end) end def handle_call({:discovery_document, provider}, _from, state) do @@ -45,6 +46,10 @@ defmodule OpenIDConnect.Worker do {:reply, config, state} end + def handle_cast({:reconfigure, provider_configs}, _state) do + {:noreply, build_state(provider_configs)} + end + def handle_info({:update_documents, provider}, state) do provider = Map.fetch!(state, provider) config = provider.config diff --git a/test/openid_connect/worker_test.exs b/test/openid_connect/worker_test.exs index 5db3d8f..1732d54 100644 --- a/test/openid_connect/worker_test.exs +++ b/test/openid_connect/worker_test.exs @@ -10,8 +10,8 @@ defmodule OpenIDConnect.WorkerTest do alias OpenIDConnect.HTTPClientMock - test "starting with :ignore does nothing" do - :ignore = OpenIDConnect.Worker.start_link(:ignore) + test "starting with :ignore creates a worker with empty state" do + {:ok, _pid} = OpenIDConnect.Worker.start_link(:ignore) end test "starting with a single provider will retrieve the necessary documents" do diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index 5bebf85..0d4665e 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -526,6 +526,27 @@ defmodule OpenIDConnectTest do end end + describe "reconfigure/2" do + test "updates provider configs" do + {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + + vault_document = + Fixtures.load(:vault, :discovery_document) + |> elem(1) + |> Map.get(:body) + |> Jason.decode!() + |> OpenIDConnect.normalize_discovery_document() + + state = %{ + document: vault_document + } + + OpenIDConnect.reconfigure(state) + + assert :sys.get_state(pid) == state + end + end + defp set_jose_json_lib(_) do JOSE.json_module(JasonEncoder) [] diff --git a/test/support/mock_worker.ex b/test/support/mock_worker.ex index edf359d..bbf622d 100644 --- a/test/support/mock_worker.ex +++ b/test/support/mock_worker.ex @@ -16,20 +16,27 @@ defmodule OpenIDConnect.MockWorker do |> Jason.decode!() |> JOSE.JWK.from() - def init(_) do + def init(_opts) do + {:ok, build_state()} + end + + defp build_state do {"google", config} = Application.get_env(:openid_connect, :providers) |> List.keyfind("google", 0) - {:ok, - %{ - config: config, - jwk: @google_jwk, - document: @google_document - }} + %{ + config: config, + jwk: @google_jwk, + document: @google_document + } + end + + def handle_cast({:reconfigure, state}, _state) do + {:noreply, state} end - def handle_call({:discovery_document, "google"}, _from, state) do + def handle_call({:discovery_document, _provider}, _from, state) do {:reply, Map.get(state, :document), state} end From 74b407ce25f54255a3f9ee35854e08839b3dafa3 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Mon, 9 Jan 2023 18:17:56 -0600 Subject: [PATCH 13/32] Add a :flush message handler to let tests await for all messaged to be processed --- lib/openid_connect/worker.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/openid_connect/worker.ex b/lib/openid_connect/worker.ex index 4b3b24f..11248ab 100644 --- a/lib/openid_connect/worker.ex +++ b/lib/openid_connect/worker.ex @@ -28,6 +28,10 @@ defmodule OpenIDConnect.Worker do end) end + def handle_call(:flush, _from, state) do + {:reply, state, state} + end + def handle_call({:discovery_document, provider}, _from, state) do provider = Map.fetch!(state, provider) discovery_document = provider.documents.discovery_document From 9a1cc884e2004f028ce1f0ef52ad1ed372925a06 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Wed, 11 Jan 2023 11:45:22 -0600 Subject: [PATCH 14/32] Complete rewrite of the library The library was storing a state in a GenServer which is causing various issues: 1. When configuration is changed we needed to restart the process having a small OIDC downtime during the process; 2. There were no way to run async tests because GenServer was querying discovery document URL's on the background, which means we wasn't able to use Bypass in integration tests 3. Storing a lot of configuration on the library side would cause a lot of issues once we have multi-tenancy and a ton of different configuration, the GenServer will be a bottleneck. --- lib/openid_connect.ex | 433 ++++------- lib/openid_connect/application.ex | 15 + lib/openid_connect/document.ex | 143 ++++ lib/openid_connect/document/cache.ex | 91 +++ lib/openid_connect/worker.ex | 82 -- mix.exs | 21 +- mix.lock | 18 +- .../http/auth0/discovery_document.exs | 15 +- test/fixtures/http/auth0/jwks.exs | 15 +- .../http/azure/discovery_document.exs | 16 +- test/fixtures/http/azure/jwks.exs | 16 +- .../http/google/discovery_document.exs | 14 +- test/fixtures/http/google/jwks.exs | 16 +- .../http/keycloak/discovery_document.exs | 16 +- test/fixtures/http/keycloak/jwks.exs | 16 +- .../fixtures/http/okta/discovery_document.exs | 14 +- test/fixtures/http/okta/jwks.exs | 14 +- .../http/onelogin/discovery_document.exs | 14 +- test/fixtures/http/onelogin/jwks.exs | 14 +- .../http/vault/discovery_document.exs | 18 +- test/fixtures/http/vault/jwks.exs | 16 +- test/fixtures/jwks/{jwk1.exs => jwk.exs} | 0 test/fixtures/jwks/jwk2.exs | 11 - test/fixtures/jwks/jwks.exs | 26 - test/openid_connect/document/cache_test.exs | 139 ++++ test/openid_connect/document_test.exs | 195 +++++ test/openid_connect/worker_test.exs | 125 --- test/openid_connect_test.exs | 712 ++++++------------ test/support/fixtures.ex | 22 - test/support/fixutes.ex | 37 + test/support/jason_encoder.ex | 13 - test/support/mock_worker.ex | 58 -- test/test_helper.exs | 2 - 33 files changed, 1060 insertions(+), 1297 deletions(-) create mode 100644 lib/openid_connect/application.ex create mode 100644 lib/openid_connect/document.ex create mode 100644 lib/openid_connect/document/cache.ex delete mode 100644 lib/openid_connect/worker.ex rename test/fixtures/jwks/{jwk1.exs => jwk.exs} (100%) delete mode 100644 test/fixtures/jwks/jwk2.exs delete mode 100644 test/fixtures/jwks/jwks.exs create mode 100644 test/openid_connect/document/cache_test.exs create mode 100644 test/openid_connect/document_test.exs delete mode 100644 test/openid_connect/worker_test.exs delete mode 100644 test/support/fixtures.ex create mode 100644 test/support/fixutes.ex delete mode 100644 test/support/jason_encoder.ex delete mode 100644 test/support/mock_worker.ex diff --git a/lib/openid_connect.ex b/lib/openid_connect.ex index e0f7cbb..d959624 100644 --- a/lib/openid_connect.ex +++ b/lib/openid_connect.ex @@ -2,78 +2,66 @@ defmodule OpenIDConnect do @moduledoc """ Handles a majority of the life-cycle concerns with [OpenID Connect](http://openid.net/connect/) """ + alias OpenIDConnect.Document @typedoc """ - URI as a string + URL to a [OpenID Discovery Document](https://openid.net/specs/openid-connect-discovery-1_0.html) endpoint. """ - @type uri :: String.t() + @type discovery_document_uri :: String.t() @typedoc """ - JSON Web Token - - See: https://jwt.io/introduction/ + OAuth 2.0 Client Identifier valid at the Authorization Server. """ - @type jwt :: String.t() + @type client_id :: String.t() @typedoc """ - The provider name as an atom - - Example: `:google` - - This atom should match what you've used in your application config + OAuth 2.0 Client Secret valid at the Authorization Server. """ - @type provider :: atom + @type client_secret :: String.t() @typedoc """ - The payload of user data from the provider - """ - @type claims :: map + Redirection URI to which the response will be sent. - @typedoc """ - The name of the genserver + This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider, + with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison). - This is optional and will default to `:openid_connect` unless overridden + When using this flow, the Redirection URI SHOULD use the https scheme; however, it MAY use the http scheme, + provided that the Client Type is confidential, as defined in Section 2.1 of OAuth 2.0, + and provided the OP allows the use of http Redirection URIs in this case. The Redirection URI MAY use an alternate scheme, + such as one that is intended to identify a callback into a native application. """ - @type name :: atom + @type redirect_uri :: String.t() @typedoc """ - Query param map + OAuth 2.0 Response Type value that determines the authorization processing flow to be used, + including what parameters are returned from the endpoints used. """ - @type params :: map - - @typedoc """ - The success tuple + @type response_type :: [String.t()] | String.t() - The 2nd element will be the relevant value to work with - """ - @type success(value) :: {:ok, value} @typedoc """ - A string reason for an error failure + OAuth 2.0 Scope Values that the Client is declaring that it will restrict itself to using. """ - @type reason :: String.t() | %HTTPoison.Error{} | %HTTPoison.Response{} + @type scope :: [String.t()] | String.t() @typedoc """ - An error tuple - - The 2nd element will indicate which function failed - The 3rd element will give details of the failure + The configuration of a OpenID provider. """ - @type error(name) :: {:error, name, reason} + @type config :: %{ + required(:discovery_document_uri) => discovery_document_uri(), + required(:client_id) => client_id(), + required(:client_secret) => client_secret(), + required(:redirect_uri) => redirect_uri(), + required(:response_type) => response_type(), + required(:scope) => scope() + } @typedoc """ - A provider's documents + JSON Web Token - * discovery_document: the provider's discovery document for OpenID Connect - * jwk: the provider's certificates converted into a JOSE JSON Web Key - * remaining_lifetime: how long the provider's JWK is valid for + See: https://jwt.io/introduction/ """ - @type documents :: %{ - discovery_document: map, - jwk: JOSE.JWK.t(), - remaining_lifetime: integer | nil - } + @type jwt :: String.t() - @spec authorization_uri(provider, params, name) :: uri @doc """ Builds the authorization URI according to the spec in the providers discovery document @@ -85,27 +73,63 @@ defmodule OpenIDConnect do > It is *highly suggested* that you add the `state` param for security reasons. Your > OpenID Connect provider should have more information on this topic. """ - def authorization_uri(provider, params \\ %{}, name \\ :openid_connect) do - document = discovery_document(provider, name) - config = config(provider, name) + @spec authorization_uri(config(), params :: %{optional(atom) => term()}) :: + {:ok, uri :: String.t()} | {:error, term()} + def authorization_uri(config, params \\ %{}) do + discovery_document_uri = config.discovery_document_uri + + with {:ok, document} <- Document.fetch_document(discovery_document_uri), + {:ok, response_type} <- fetch_response_type(config, document), + {:ok, scope} <- fetch_scope(config) do + params = + Map.merge( + %{ + client_id: config.client_id, + redirect_uri: config.redirect_uri, + response_type: response_type, + scope: scope + }, + params + ) + + {:ok, build_uri(document.authorization_endpoint, params)} + end + end - uri = Map.get(document, "authorization_endpoint") + defp fetch_scope(%{scope: scope}) when is_nil(scope) or scope == [], + do: {:error, :invalid_scope} - params = - Map.merge( - %{ - client_id: client_id(config), - redirect_uri: redirect_uri(config), - response_type: response_type(provider, config, name), - scope: normalize_scope(provider, config[:scope]) - }, - params - ) + defp fetch_scope(%{scope: scope}) when is_binary(scope), + do: {:ok, scope} + + defp fetch_scope(%{scope: scopes}) when is_list(scopes), + do: {:ok, Enum.join(scopes, " ")} - build_uri(uri, params) + defp fetch_response_type( + %{response_type: response_type}, + %Document{response_types_supported: response_types_supported} + ) do + with {:ok, response_type} <- parse_response_type(response_type) do + response_type = Enum.sort(response_type) + + if Enum.all?(response_type, &(&1 in response_types_supported)) do + {:ok, Enum.join(response_type, " ")} + else + {:error, + {:response_type_not_supported, response_types_supported: response_types_supported}} + end + end end - @spec end_session_uri(provider, params, name) :: uri + defp parse_response_type(nil), do: {:error, :invalid_response_type} + defp parse_response_type([]), do: {:error, :invalid_response_type} + + defp parse_response_type(response_type) when is_binary(response_type), + do: {:ok, String.split(response_type)} + + defp parse_response_type(response_type) when is_list(response_type), + do: {:ok, response_type} + @doc """ Builds the end session URI according to the spec in the providers discovery document @@ -119,25 +143,25 @@ defmodule OpenIDConnect do Each provider will typically require one or more of the supported query params, e.g. `id_token_hint` or `client_id`. Read your provider's OIDC documentation to determine which one(s) you should add. - """ - def end_session_uri(provider, params \\ %{}, name \\ :openid_connect) do - document = discovery_document(provider, name) - config = config(provider, name) - - uri = Map.get(document, "end_session_endpoint") - - params = - Map.merge( - %{ - client_id: client_id(config) - }, - params - ) - build_uri(uri, params) + Some providers don't specify `end_session_endpoint` in their discovery documents, + in such cases `{:error, :endpoint_not_set}` is returned. + """ + @spec end_session_uri(config(), params :: %{optional(atom) => term()}) :: + {:ok, uri :: String.t()} | {:error, term()} + def end_session_uri(config, params \\ %{}) do + discovery_document_uri = config.discovery_document_uri + + with {:ok, document} <- Document.fetch_document(discovery_document_uri) do + if end_session_endpoint = document.end_session_endpoint do + params = Map.merge(%{client_id: config.client_id}, params) + {:ok, build_uri(end_session_endpoint, params)} + else + {:error, :endpoint_not_set} + end + end end - @spec fetch_tokens(provider, params, name) :: success(map) | error(:fetch_tokens) @doc """ Fetches the authentication tokens from the provider @@ -145,133 +169,77 @@ defmodule OpenIDConnect do was requested during authorization. `params` may also include any one-off overrides for token fetching. """ - def fetch_tokens(provider, params, name \\ :openid_connect) - - def fetch_tokens(provider, code, name) when is_binary(code) do - IO.warn( - "Deprecation: `OpenIDConnect.fetch_tokens/3` no longer takes a binary as the 2nd argument. Please refer to the docs for the new API." - ) - - fetch_tokens(provider, %{code: code}, name) - end - - def fetch_tokens(provider, params, name) do - uri = access_token_uri(provider, name) - config = config(provider, name) + @spec fetch_tokens(config(), params :: %{optional(atom) => term()}) :: + {:ok, response :: map()} | {:error, term()} + def fetch_tokens(config, params) do + discovery_document_uri = config.discovery_document_uri form_body = Map.merge( %{ - client_id: client_id(config), - client_secret: client_secret(config), - grant_type: "authorization_code", - redirect_uri: redirect_uri(config) + client_id: config.client_id, + client_secret: config.client_secret, + redirect_uri: config.redirect_uri, + grant_type: "authorization_code" }, params ) - |> Map.to_list() + |> URI.encode_query(:www_form) headers = [{"Content-Type", "application/x-www-form-urlencoded"}] - with {:ok, %HTTPoison.Response{status_code: status_code} = resp} when status_code in 200..299 <- - http_client().post(uri, {:form, form_body}, headers, http_client_options()), - {:ok, json} <- Jason.decode(resp.body), - {:ok, json} <- assert_json(json) do + with {:ok, document} <- Document.fetch_document(discovery_document_uri), + request = Finch.build(:post, document.token_endpoint, headers, form_body), + {:ok, %Finch.Response{body: response, status: status}} when status in 200..299 <- + Finch.request(request, OpenIDConnect.Finch), + {:ok, json} <- Jason.decode(response) do {:ok, json} else - {:ok, resp} -> {:error, :fetch_tokens, resp} - {:error, reason} -> {:error, :fetch_tokens, reason} + {:ok, %Finch.Response{body: response, status: status}} -> {:error, {status, response}} + other -> other end end - @spec verify(provider, jwt, name) :: success(claims) | error(:verify) @doc """ Verifies the validity of the JSON Web Token (JWT) This verification will assert the token's encryption against the provider's JSON Web Key (JWK) """ - def verify(provider, jwt, name \\ :openid_connect) do - jwk = jwk(provider, name) + @spec verify(config(), jwt :: String.t()) :: + {:ok, claims :: map()} | {:error, term()} + def verify(config, jwt) do + discovery_document_uri = config.discovery_document_uri with {:ok, protected} <- peek_protected(jwt), {:ok, decoded_protected} <- Jason.decode(protected), {:ok, token_alg} <- Map.fetch(decoded_protected, "alg"), - {true, claims, _jwk} <- do_verify(jwk, token_alg, jwt) do + {:ok, document} <- Document.fetch_document(discovery_document_uri), + {true, claims, _jwk} <- do_verify(document.jwks, token_alg, jwt) do Jason.decode(claims) else {:error, %Jason.DecodeError{}} -> - {:error, :verify, "token claims did not contain a JSON payload"} + {:error, {:invalid_jwt, "token claims did not contain a JSON payload"}} {:error, :peek_protected} -> - {:error, :verify, "invalid token format"} + {:error, {:invalid_jwt, "invalid token format"}} :error -> - {:error, :verify, "no `alg` found in token"} + {:error, {:invalid_jwt, "no `alg` found in token"}} {false, _claims, _jwk} -> - {:error, :verify, "verification failed"} - - _ -> - {:error, :verify, "verification error"} - end - end + {:error, {:invalid_jwt, "verification failed"}} - @spec update_documents(list) :: success(documents) | error(:update_documents) - @doc """ - Requests updated documents from the provider + {:error, {:case_clause, _}} -> + {:error, {:invalid_jwt, "verification failed"}} - This function is used by `OpenIDConnect.Worker` for document updates - according to the lifetime returned by the provider - """ - def update_documents(config) do - uri = discovery_document_uri(config) - - with {:ok, discovery_document, _} <- fetch_resource(uri), - {:ok, certs, remaining_lifetime} <- fetch_resource(discovery_document["jwks_uri"]), - {:ok, jwk} <- from_certs(certs) do - {:ok, - %{ - discovery_document: normalize_discovery_document(discovery_document), - jwk: jwk, - remaining_lifetime: remaining_lifetime - }} - else - {:error, reason} -> {:error, :update_documents, reason} + other -> + other end end - def reconfigure(provider_configs, name \\ :openid_connect) do - GenServer.cast(name, {:reconfigure, provider_configs}) - end - - @doc false - def normalize_discovery_document(discovery_document) do - # claims_supported may be missing as it is marked RECOMMENDED by the spec, default to an empty list - sorted_claims_supported = - discovery_document - |> Map.get("claims_supported", []) - |> Enum.sort() - - # response_types_supported's presence is REQUIRED by the spec, crash when missing - sorted_response_types_supported = - discovery_document - |> Map.get("response_types_supported") - |> Enum.map(fn response_type -> - response_type - |> String.split() - |> Enum.sort() - |> Enum.join(" ") - end) - - Map.merge(discovery_document, %{ - "claims_supported" => sorted_claims_supported, - "response_types_supported" => sorted_response_types_supported - }) - end - - defp peek_protected(jwt) do - {:ok, JOSE.JWS.peek_protected(jwt)} + defp peek_protected(jwks) do + {:ok, JOSE.JWS.peek_protected(jwks)} rescue _ -> {:error, :peek_protected} end @@ -291,100 +259,6 @@ defmodule OpenIDConnect do defp do_verify(%JOSE.JWK{} = jwk, token_alg, jwt), do: JOSE.JWS.verify_strict(jwk, [token_alg], jwt) - defp from_certs(certs) do - {:ok, JOSE.JWK.from(certs)} - rescue - _ -> - {:error, "certificates bad format"} - end - - defp discovery_document(provider, name) do - GenServer.call(name, {:discovery_document, provider}) - end - - defp jwk(provider, name) do - GenServer.call(name, {:jwk, provider}) - end - - defp config(provider, name) do - GenServer.call(name, {:config, provider}) - end - - defp access_token_uri(provider, name) do - Map.get(discovery_document(provider, name), "token_endpoint") - end - - defp client_id(config) do - Keyword.get(config, :client_id) - end - - defp client_secret(config) do - Keyword.get(config, :client_secret) - end - - defp redirect_uri(config) do - Keyword.get(config, :redirect_uri) - end - - defp response_type(provider, config, name) do - response_type = - config - |> Keyword.get(:response_type) - |> normalize_response_type(provider) - - response_types_supported = response_types_supported(provider, name) - - if response_type in response_types_supported do - response_type - else - raise ArgumentError, - message: """ - Requested response type (#{response_type}) not supported by provider (#{provider}). - Supported types: - #{Enum.join(response_types_supported, "\n")} - """ - end - end - - defp normalize_response_type(response_type, provider) - when is_nil(response_type) or response_type == [] do - raise ArgumentError, "no response_type has been defined for provider `#{provider}`" - end - - defp normalize_response_type(response_type, provider) when is_binary(response_type) do - response_type - |> String.split() - |> normalize_response_type(provider) - end - - defp normalize_response_type(response_type, _provider) when is_list(response_type) do - response_type - |> Enum.sort() - |> Enum.join(" ") - end - - defp response_types_supported(provider, name) do - provider - |> discovery_document(name) - |> Map.get("response_types_supported") - end - - defp discovery_document_uri(config) do - Keyword.get(config, :discovery_document_uri) - end - - defp fetch_resource(uri) do - with {:ok, %HTTPoison.Response{status_code: status_code} = resp} when status_code in 200..299 <- - http_client().get(uri, [], http_client_options()), - {:ok, json} <- Jason.decode(resp.body), - {:ok, json} <- assert_json(json) do - {:ok, json, remaining_lifetime(resp.headers)} - else - {:ok, resp} -> {:error, resp} - error -> error - end - end - defp build_uri(nil, _params), do: nil defp build_uri(uri, params) do @@ -394,47 +268,4 @@ defmodule OpenIDConnect do |> URI.merge("?#{query}") |> URI.to_string() end - - defp assert_json(%{"error" => reason}), do: {:error, reason} - defp assert_json(json), do: {:ok, json} - - @spec remaining_lifetime([{String.t(), String.t()}]) :: integer | nil - defp remaining_lifetime(headers) do - with headers <- Enum.into(headers, %{}), - {:ok, max_age} <- find_max_age(headers), - {:ok, age} <- find_age(headers) do - max_age - age - else - _ -> nil - end - end - - defp normalize_scope(provider, scopes) when is_nil(scopes) or scopes == [] do - raise ArgumentError, "no scopes have been defined for provider `#{provider}`" - end - - defp normalize_scope(_provider, scopes) when is_binary(scopes), do: scopes - defp normalize_scope(_provider, scopes) when is_list(scopes), do: Enum.join(scopes, " ") - - defp find_max_age(headers) when is_map(headers) do - case Regex.run(~r"(?<=max-age=)\d+", Map.get(headers, "Cache-Control", "")) do - [max_age] -> {:ok, String.to_integer(max_age)} - _ -> :error - end - end - - defp find_age(headers) when is_map(headers) do - case Map.get(headers, "Age") do - nil -> :error - age -> {:ok, String.to_integer(age)} - end - end - - defp http_client do - Application.get_env(:openid_connect, :http_client, HTTPoison) - end - - defp http_client_options do - Application.get_env(:openid_connect, :http_client_options, []) - end end diff --git a/lib/openid_connect/application.ex b/lib/openid_connect/application.ex new file mode 100644 index 0000000..ba6f5d9 --- /dev/null +++ b/lib/openid_connect/application.ex @@ -0,0 +1,15 @@ +defmodule OpenIDConnect.Application do + use Application + + def start(_type, _args) do + opts = [strategy: :one_for_one, name: FzVpn.Supervisor] + Supervisor.start_link(children(), opts) + end + + defp children do + [ + {Finch, name: OpenIDConnect.Finch}, + OpenIDConnect.Document.Cache + ] + end +end diff --git a/lib/openid_connect/document.ex b/lib/openid_connect/document.ex new file mode 100644 index 0000000..45a1eb5 --- /dev/null +++ b/lib/openid_connect/document.ex @@ -0,0 +1,143 @@ +defmodule OpenIDConnect.Document do + @doc """ + This module caches OIDC documents and their JWKs for a limited timeframe, which is min(`@refresh_time`, `document.remaining_lifetime`). + """ + alias OpenIDConnect.Document.Cache + + defstruct raw: nil, + authorization_endpoint: nil, + end_session_endpoint: nil, + token_endpoint: nil, + claims_supported: nil, + response_types_supported: nil, + jwks: nil, + expires_at: nil + + @refresh_time_seconds Application.compile_env( + :openid_connect, + :document_max_expiration_seconds, + 60 * 60 + ) + + def fetch_document(uri) do + with :error <- Cache.fetch(uri), + {:ok, document_json, document_expires_at} <- fetch_remote_resource(uri), + {:ok, document} <- build_document(document_json), + {:ok, jwks_json, jwks_expires_at} <- fetch_remote_resource(document_json["jwks_uri"]), + {:ok, jwks} <- from_certs(jwks_json) do + now = DateTime.utc_now() + + expires_at = + [ + DateTime.add(now, @refresh_time_seconds, :second), + document_expires_at, + jwks_expires_at + ] + |> Enum.reject(&is_nil/1) + |> Enum.min(DateTime) + + document = %{ + document + | jwks: jwks, + expires_at: expires_at + } + + _ = Cache.put(uri, document) + + {:ok, document} + end + end + + defp fetch_remote_resource(uri) when is_nil(uri), do: {:error, :invalid_discovery_document_uri} + + defp fetch_remote_resource(uri) do + request = Finch.build(:get, uri) + + with {:ok, + %Finch.Response{ + headers: headers, + body: response, + status: status + }} + when status in 200..299 <- + Finch.request(request, OpenIDConnect.Finch), + {:ok, json} <- Jason.decode(response) do + expires_at = + if remaining_lifetime = remaining_lifetime(headers) do + DateTime.add(DateTime.utc_now(), remaining_lifetime, :second) + end + + {:ok, json, expires_at} + else + {:ok, %Finch.Response{body: response, status: status}} -> {:error, {status, response}} + other -> other + end + end + + defp remaining_lifetime(headers) do + headers = + for {k, v} <- headers, into: %{} do + {String.downcase(k), v} + end + + max_age = get_max_age(headers) + age = get_age(headers) + + cond do + not is_nil(max_age) and max_age > 0 and not is_nil(age) -> max_age - age + not is_nil(max_age) and max_age > 0 -> max_age + true -> nil + end + end + + defp get_max_age(headers) when is_map(headers) do + cache_control = Map.get(headers, "cache-control", "") + + case Regex.run(~r"(?<=max-age=)\d+", cache_control) do + [max_age] -> String.to_integer(max_age) + _ -> nil + end + end + + defp get_age(headers) when is_map(headers) do + case Map.get(headers, "age") do + nil -> nil + age -> String.to_integer(age) + end + end + + defp build_document(document_json) do + keys = Map.keys(document_json) + required_keys = ["jwks_uri", "authorization_endpoint", "token_endpoint"] + + if Enum.all?(required_keys, &(&1 in keys)) do + document = %__MODULE__{ + raw: document_json, + authorization_endpoint: Map.fetch!(document_json, "authorization_endpoint"), + end_session_endpoint: Map.get(document_json, "end_session_endpoint"), + token_endpoint: Map.fetch!(document_json, "token_endpoint"), + response_types_supported: + Map.get(document_json, "response_types_supported") + |> Enum.map(fn response_type -> + response_type + |> String.split() + |> Enum.sort() + |> Enum.join(" ") + end), + claims_supported: + Map.get(document_json, "claims_supported") + |> Enum.sort() + } + + {:ok, document} + else + {:error, :invalid_document} + end + end + + defp from_certs(certs) do + {:ok, JOSE.JWK.from(certs)} + rescue + _ -> {:error, :invalid_jwks_certificated} + end +end diff --git a/lib/openid_connect/document/cache.ex b/lib/openid_connect/document/cache.ex new file mode 100644 index 0000000..cf7869b --- /dev/null +++ b/lib/openid_connect/document/cache.ex @@ -0,0 +1,91 @@ +defmodule OpenIDConnect.Document.Cache do + use GenServer + alias OpenIDConnect.Document + + @max_size Application.compile_env(:openid_connect, :document_cache_max_size, 1_000) + + def start_link(opts \\ []) do + {name, opts} = Keyword.pop(opts, :name, __MODULE__) + GenServer.start_link(__MODULE__, opts, name: name) + end + + def init(_opts) do + Process.send_after(self(), :gc, :timer.minutes(1)) + {:ok, %{}} + end + + def put(pid \\ __MODULE__, uri, document) do + # TODO: we need to update timer in case the new document is expiring before it will exceed + GenServer.cast(pid, {:put, uri, document}) + end + + def fetch(pid \\ __MODULE__, uri) do + GenServer.call(pid, {:fetch, uri}) + end + + def flush(pid \\ __MODULE__) do + GenServer.call(pid, :flush) + end + + def handle_cast({:put, uri, document}, state) do + if not document_expired?(document) do + expires_in_seconds = expires_in_seconds(document.expires_at) + timer_ref = Process.send_after(self(), :remove, :timer.seconds(expires_in_seconds)) + state = Map.put(state, uri, {timer_ref, DateTime.utc_now(), document}) + {:noreply, state} + else + {:noreply, state} + end + end + + def handle_call(:flush, _from, state) do + {:reply, state, state} + end + + def handle_call({:fetch, uri}, _from, state) do + case Map.fetch(state, uri) do + {:ok, {timer_ref, _last_fetched_at, document}} -> + if document_expired?(document) do + state = Map.delete(state, uri) + {:reply, :error, state} + else + state = Map.put(state, uri, {timer_ref, DateTime.utc_now(), document}) + {:reply, {:ok, document}, state} + end + + :error -> + {:reply, :error, state} + end + end + + def handle_info({:remove, uri}, state) do + {:noreply, Map.delete(state, uri)} + end + + def handle_info(:gc, state) do + state = + if Enum.count(state) > @max_size do + state + |> Enum.sort_by( + fn {_key, {_ref, last_fetched_at, _document}} -> last_fetched_at end, + {:desc, DateTime} + ) + |> Enum.take(@max_size) + |> Enum.into(%{}) + else + state + end + + Process.send_after(self(), :gc, :timer.minutes(1)) + + {:noreply, state} + end + + defp expires_in_seconds(%DateTime{} = datetime) do + max(DateTime.diff(datetime, DateTime.utc_now(), :second), 0) + end + + defp document_expired?(%Document{expires_at: expires_at}) do + DateTime.compare(expires_at, DateTime.utc_now()) != :gt + end +end diff --git a/lib/openid_connect/worker.ex b/lib/openid_connect/worker.ex deleted file mode 100644 index 11248ab..0000000 --- a/lib/openid_connect/worker.ex +++ /dev/null @@ -1,82 +0,0 @@ -defmodule OpenIDConnect.Worker do - use GenServer - - @moduledoc """ - Worker module for OpenID Connect - - This worker will store and periodically update each provider's documents and JWKs according to the lifetimes - """ - - @refresh_time 60 * 60 * 1000 - - def start_link(provider_configs, name \\ :openid_connect) do - GenServer.start_link(__MODULE__, provider_configs, name: name) - end - - def init(:ignore) do - {:ok, []} - end - - def init(provider_configs) do - {:ok, build_state(provider_configs)} - end - - defp build_state(provider_configs) do - Enum.into(provider_configs, %{}, fn {provider, config} -> - documents = update_documents(provider, config) - {provider, %{config: config, documents: documents}} - end) - end - - def handle_call(:flush, _from, state) do - {:reply, state, state} - end - - def handle_call({:discovery_document, provider}, _from, state) do - provider = Map.fetch!(state, provider) - discovery_document = provider.documents.discovery_document - {:reply, discovery_document, state} - end - - def handle_call({:jwk, provider}, _from, state) do - provider = Map.fetch!(state, provider) - jwk = provider.documents.jwk - {:reply, jwk, state} - end - - def handle_call({:config, provider}, _from, state) do - provider = Map.fetch!(state, provider) - config = provider.config - {:reply, config, state} - end - - def handle_cast({:reconfigure, provider_configs}, _state) do - {:noreply, build_state(provider_configs)} - end - - def handle_info({:update_documents, provider}, state) do - provider = Map.fetch!(state, provider) - config = provider.config - documents = update_documents(provider, config) - state = Map.put(state, provider, %{provider | documents: documents}) - {:noreply, state} - end - - defp update_documents(provider, config) do - {:ok, %{remaining_lifetime: remaining_lifetime} = documents} = - OpenIDConnect.update_documents(config) - - refresh_time = time_until_next_refresh(remaining_lifetime) - - Process.send_after(self(), {:update_documents, provider}, refresh_time) - - documents - end - - defp time_until_next_refresh(nil), do: @refresh_time - - defp time_until_next_refresh(time_in_seconds) when time_in_seconds > 0, - do: :timer.seconds(time_in_seconds) - - defp time_until_next_refresh(time_in_seconds) when time_in_seconds <= 0, do: 0 -end diff --git a/mix.exs b/mix.exs index a60c181..353ccc4 100644 --- a/mix.exs +++ b/mix.exs @@ -1,13 +1,13 @@ defmodule OpenIDConnect.Mixfile do use Mix.Project - @version "0.2.2" + @version "1.0.0" def project do [ app: :openid_connect, version: @version, - elixir: "~> 1.3", + elixir: "~> 1.13", build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), @@ -28,14 +28,14 @@ defmodule OpenIDConnect.Mixfile do ] end - # Specifies which paths to compile per environment defp elixirc_paths(:test), do: elixirc_paths(nil) ++ ["test/support"] defp elixirc_paths(_), do: ["lib"] - # Configuration for the OTP application - # - # Type "mix help compile.app" for more information + def application do - [extra_applications: [:logger]] + [ + mod: {OpenIDConnect.Application, []}, + extra_applications: [:logger] + ] end def description do @@ -64,15 +64,18 @@ defmodule OpenIDConnect.Mixfile do defp deps do [ - {:httpoison, "~> 1.2"}, {:jason, ">= 1.0.0"}, + {:finch, "~> 0.14"}, {:jose, "~> 1.8"}, + + # Test deps {:earmark, "~> 1.2", only: :dev}, {:credo, "~> 1.6", only: :dev}, {:dialyxir, "~> 1.2", only: :dev}, {:ex_doc, "~> 0.18", only: :dev}, {:excoveralls, "~> 0.14", only: :test}, - {:mox, "~> 1.0", only: :test} + {:plug_cowboy, "~> 2.6", only: :test}, + {:bypass, "~> 2.1", only: :test} ] end end diff --git a/mix.lock b/mix.lock index ebffc44..c9b0072 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,12 @@ %{ "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, + "castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, "cutkey": {:git, "https://github.com/potatosalad/cutkey.git", "47640d04fb4db4a0b79168d7fca0df87aaa42751", []}, "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, @@ -11,8 +16,9 @@ "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"}, "excoveralls": {:hex, :excoveralls, "0.15.0", "ac941bf85f9f201a9626cc42b2232b251ad8738da993cf406a4290cacf562ea4", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9631912006b27eca30a2f3c93562bc7ae15980afb014ceb8147dc5cdd8f376f1"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, - "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"}, @@ -20,11 +26,19 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, + "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, + "nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "telemetry": {:hex, :telemetry, "1.2.0", "a8ce551485a9a3dac8d523542de130eafd12e40bbf76cf0ecd2528f24e812a44", [:rebar3], [], "hexpm", "1427e73667b9a2002cf1f26694c422d5c905df889023903c4518921d53e3e883"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/fixtures/http/auth0/discovery_document.exs b/test/fixtures/http/auth0/discovery_document.exs index 1a5a1dc..3a1ace9 100644 --- a/test/fixtures/http/auth0/discovery_document.exs +++ b/test/fixtures/http/auth0/discovery_document.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "authorization_endpoint" => "https://common.auth0.com/authorize", @@ -64,8 +64,6 @@ headers: [ {"Date", "Sat, 12 Nov 2022 19:57:59 GMT"}, {"Content-Type", "application/json; charset=utf-8"}, - {"Transfer-Encoding", "chunked"}, - {"Connection", "keep-alive"}, {"CF-Ray", "7691d6fb8d50f96b-SJC"}, {"Access-Control-Allow-Origin", "*"}, {"Cache-Control", "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"}, @@ -89,14 +87,5 @@ {"X-RateLimit-Reset", "1668283080"}, {"Server", "cloudflare"}, {"alt-svc", "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400"} - ], - request_url: "https://common.auth0.com/.well-known/openid-configuration", - request: %HTTPoison.Request{ - method: :get, - url: "https://common.auth0.com/.well-known/openid-configuration", - headers: [], - body: "", - params: %{}, - options: [] - } + ] } diff --git a/test/fixtures/http/auth0/jwks.exs b/test/fixtures/http/auth0/jwks.exs index e129656..fe41812 100644 --- a/test/fixtures/http/auth0/jwks.exs +++ b/test/fixtures/http/auth0/jwks.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "keys" => [ @@ -33,8 +33,6 @@ headers: [ {"Date", "Sat, 12 Nov 2022 19:58:26 GMT"}, {"Content-Type", "application/json; charset=utf-8"}, - {"Transfer-Encoding", "chunked"}, - {"Connection", "keep-alive"}, {"CF-Ray", "7691d7a5d88df96b-SJC"}, {"Access-Control-Allow-Origin", "*"}, {"Cache-Control", "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"}, @@ -58,14 +56,5 @@ {"X-RateLimit-Reset", "1668283107"}, {"Server", "cloudflare"}, {"alt-svc", "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400"} - ], - request_url: "https://common.auth0.com/.well-known/jwks.json", - request: %HTTPoison.Request{ - method: :get, - url: "https://common.auth0.com/.well-known/jwks.json", - headers: [], - body: "", - params: %{}, - options: [] - } + ] } diff --git a/test/fixtures/http/azure/discovery_document.exs b/test/fixtures/http/azure/discovery_document.exs index cd7d1c8..ec6857a 100644 --- a/test/fixtures/http/azure/discovery_document.exs +++ b/test/fixtures/http/azure/discovery_document.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "authorization_endpoint" => "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", @@ -67,16 +67,6 @@ "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7Wevr2ALKzZMjPY-Tt7ffB-f_7y4AMTUR-4m-AQDAi0jJ1K4_N7dY0CZmKZdSweQPMgerZ-TeKnty43nfmYRZS2G39bKUZp5erQLwiB9rkuLis4_ee_cAZK7nh1pkqOh0_t52P9svf75Le0-ex8iyPVhexTbIROTaaYvo6Fl9DFqOtZOnmQplc6ken-ddUcLbnZRSKOTFdr03VB8oSt5gD2BBw2e5qeBuocgX0hS-W-FNbG0gAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None"}, {"Set-Cookie", "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly"}, {"Set-Cookie", "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"}, - {"Date", "Sat, 12 Nov 2022 19:36:29 GMT"}, - {"Content-Length", "1547"} - ], - request_url: "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", - request: %HTTPoison.Request{ - method: :get, - url: "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", - headers: [], - body: "", - params: %{}, - options: [] - } + {"Date", "Sat, 12 Nov 2022 19:36:29 GMT"} + ] } diff --git a/test/fixtures/http/azure/jwks.exs b/test/fixtures/http/azure/jwks.exs index e49c858..e307144 100644 --- a/test/fixtures/http/azure/jwks.exs +++ b/test/fixtures/http/azure/jwks.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "keys" => [ @@ -151,16 +151,6 @@ "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7WevrisuCv4ebeqYc-_POPk9YcVRhn9bd7CUqVj30-s0iSKalFe-d2lZ-f4-XK0n9ihY93a7m9SsGmsIMY7zD9ez66cNlKITs_ezKr4bRpFPqimcqQaHWPo_UKxjSYOQvsOH1N0F2vp-2Ht7cFowAp8vUIO0oCj1Fbps_-Di7qvstKBXVmkHe6uENMnH0BpbyPPa0DskUrFDPPwH1po1Hthps2l35xFERrP6mYU6aS4jwW98gAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None"}, {"Set-Cookie", "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly"}, {"Set-Cookie", "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"}, - {"Date", "Sat, 12 Nov 2022 20:59:21 GMT"}, - {"Content-Length", "15922"} - ], - request_url: "https://login.microsoftonline.com/common/discovery/v2.0/keys", - request: %HTTPoison.Request{ - method: :get, - url: "https://login.microsoftonline.com/common/discovery/v2.0/keys", - headers: [], - body: "", - params: %{}, - options: [] - } + {"Date", "Sat, 12 Nov 2022 20:59:21 GMT"} + ] } diff --git a/test/fixtures/http/google/discovery_document.exs b/test/fixtures/http/google/discovery_document.exs index 67be63f..e1ec2bc 100644 --- a/test/fixtures/http/google/discovery_document.exs +++ b/test/fixtures/http/google/discovery_document.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "authorization_endpoint" => "https://accounts.google.com/o/oauth2/v2/auth", @@ -53,7 +53,6 @@ {"Cross-Origin-Opener-Policy", "same-origin; report-to=\"federated-signon-mpm-access\""}, {"Report-To", "{\"group\":\"federated-signon-mpm-access\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/federated-signon-mpm-access\"}]}"}, - {"Content-Length", "1280"}, {"X-Content-Type-Options", "nosniff"}, {"Server", "sffe"}, {"X-XSS-Protection", "0"}, @@ -65,14 +64,5 @@ {"Content-Type", "application/json"}, {"Alt-Svc", "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\""} - ], - request_url: "https://accounts.google.com/.well-known/openid-configuration", - request: %HTTPoison.Request{ - method: :get, - url: "https://accounts.google.com/.well-known/openid-configuration", - headers: [], - body: "", - params: %{}, - options: [] - } + ] } diff --git a/test/fixtures/http/google/jwks.exs b/test/fixtures/http/google/jwks.exs index 0542ad2..694ed89 100644 --- a/test/fixtures/http/google/jwks.exs +++ b/test/fixtures/http/google/jwks.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "keys" => [ @@ -35,16 +35,6 @@ {"Alt-Svc", "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\""}, {"Accept-Ranges", "none"}, - {"Vary", "Origin,X-Origin,Referer,Accept-Encoding"}, - {"Transfer-Encoding", "chunked"} - ], - request_url: "https://www.googleapis.com/oauth2/v3/certs", - request: %HTTPoison.Request{ - method: :get, - url: "https://www.googleapis.com/oauth2/v3/certs", - headers: [], - body: "", - params: %{}, - options: [] - } + {"Vary", "Origin,X-Origin,Referer,Accept-Encoding"} + ] } diff --git a/test/fixtures/http/keycloak/discovery_document.exs b/test/fixtures/http/keycloak/discovery_document.exs index b37977f..6e43e83 100644 --- a/test/fixtures/http/keycloak/discovery_document.exs +++ b/test/fixtures/http/keycloak/discovery_document.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "frontchannel_logout_supported" => true, @@ -282,16 +282,6 @@ {"Cache-Control", "no-cache, must-revalidate, no-transform, no-store"}, {"X-Content-Type-Options", "nosniff"}, {"X-XSS-Protection", "1; mode=block"}, - {"Content-Type", "application/json"}, - {"content-length", "5823"} - ], - request_url: "http://localhost:8080/realms/master/.well-known/openid-configuration", - request: %HTTPoison.Request{ - method: :get, - url: "http://localhost:8080/realms/master/.well-known/openid-configuration", - headers: [], - body: "", - params: %{}, - options: [] - } + {"Content-Type", "application/json"} + ] } diff --git a/test/fixtures/http/keycloak/jwks.exs b/test/fixtures/http/keycloak/jwks.exs index 8cd16aa..d512b56 100644 --- a/test/fixtures/http/keycloak/jwks.exs +++ b/test/fixtures/http/keycloak/jwks.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "keys" => [ @@ -39,16 +39,6 @@ {"Cache-Control", "no-cache"}, {"X-Content-Type-Options", "nosniff"}, {"X-XSS-Protection", "1; mode=block"}, - {"Content-Type", "application/json"}, - {"content-length", "2917"} - ], - request_url: "http://localhost:8080/realms/master/protocol/openid-connect/certs", - request: %HTTPoison.Request{ - method: :get, - url: "http://localhost:8080/realms/master/protocol/openid-connect/certs", - headers: [], - body: "", - params: %{}, - options: [] - } + {"Content-Type", "application/json"} + ] } diff --git a/test/fixtures/http/okta/discovery_document.exs b/test/fixtures/http/okta/discovery_document.exs index c177a0f..9652d7a 100644 --- a/test/fixtures/http/okta/discovery_document.exs +++ b/test/fixtures/http/okta/discovery_document.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "authorization_endpoint" => "https://common.okta.com/oauth2/v1/authorize", @@ -109,7 +109,6 @@ headers: [ {"Date", "Sat, 12 Nov 2022 19:40:24 GMT"}, {"Content-Type", "application/json"}, - {"Transfer-Encoding", "chunked"}, {"Connection", "keep-alive"}, {"Server", "nginx"}, {"Public-Key-Pins-Report-Only", @@ -126,14 +125,5 @@ {"x-content-type-options", "nosniff"}, {"Strict-Transport-Security", "max-age=315360000; includeSubDomains"}, {"X-Okta-Request-Id", "Y2_2qFFQ3zhcoZh3312xKwAAARU"} - ], - request_url: "https://common.okta.com/.well-known/openid-configuration", - request: %HTTPoison.Request{ - method: :get, - url: "https://common.okta.com/.well-known/openid-configuration", - headers: [], - body: "", - params: %{}, - options: [] - } + ] } diff --git a/test/fixtures/http/okta/jwks.exs b/test/fixtures/http/okta/jwks.exs index b0cade4..8180266 100644 --- a/test/fixtures/http/okta/jwks.exs +++ b/test/fixtures/http/okta/jwks.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "keys" => [ @@ -25,7 +25,6 @@ headers: [ {"Date", "Sat, 12 Nov 2022 19:41:02 GMT"}, {"Content-Type", "application/json"}, - {"Transfer-Encoding", "chunked"}, {"Connection", "keep-alive"}, {"Server", "nginx"}, {"Public-Key-Pins-Report-Only", @@ -44,14 +43,5 @@ {"x-content-type-options", "nosniff"}, {"Strict-Transport-Security", "max-age=315360000; includeSubDomains"}, {"X-Okta-Request-Id", "Y2_2zY7e1--88ktkBh3QSgAABkg"} - ], - request_url: "https://common.okta.com/oauth2/v1/keys", - request: %HTTPoison.Request{ - method: :get, - url: "https://common.okta.com/oauth2/v1/keys", - headers: [], - body: "", - params: %{}, - options: [] - } + ] } diff --git a/test/fixtures/http/onelogin/discovery_document.exs b/test/fixtures/http/onelogin/discovery_document.exs index 90bd1c9..7818b0e 100644 --- a/test/fixtures/http/onelogin/discovery_document.exs +++ b/test/fixtures/http/onelogin/discovery_document.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "acr_values_supported" => ["onelogin:nist:level:1:re-auth"], @@ -66,21 +66,11 @@ headers: [ {"Date", "Sat, 12 Nov 2022 19:41:55 GMT"}, {"Content-Type", "application/json; charset=utf-8"}, - {"Content-Length", "1790"}, {"Connection", "keep-alive"}, {"vary", "Origin"}, {"strict-transport-security", "max-age=63072000; includeSubDomains;"}, {"x-content-type-options", "nosniff"}, {"set-cookie", "ol_oidc_canary_115=false; path=/; domain=.onelogin.com; HttpOnly; Secure"}, {"cache-control", "private"} - ], - request_url: "https://common.onelogin.com/oidc/2/.well-known/openid-configuration", - request: %HTTPoison.Request{ - method: :get, - url: "https://common.onelogin.com/oidc/2/.well-known/openid-configuration", - headers: [], - body: "", - params: %{}, - options: [] - } + ] } diff --git a/test/fixtures/http/onelogin/jwks.exs b/test/fixtures/http/onelogin/jwks.exs index 7842e8b..e188733 100644 --- a/test/fixtures/http/onelogin/jwks.exs +++ b/test/fixtures/http/onelogin/jwks.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "keys" => [ @@ -16,21 +16,11 @@ headers: [ {"Date", "Sat, 12 Nov 2022 19:42:26 GMT"}, {"Content-Type", "application/json; charset=utf-8"}, - {"Content-Length", "466"}, {"Connection", "keep-alive"}, {"vary", "Origin"}, {"strict-transport-security", "max-age=63072000; includeSubDomains;"}, {"x-content-type-options", "nosniff"}, {"set-cookie", "ol_oidc_canary_115=true; path=/; domain=.onelogin.com; HttpOnly; Secure"}, {"cache-control", "private"} - ], - request_url: "https://common.onelogin.com/oidc/2/certs", - request: %HTTPoison.Request{ - method: :get, - url: "https://common.onelogin.com/oidc/2/certs", - headers: [], - body: "", - params: %{}, - options: [] - } + ] } diff --git a/test/fixtures/http/vault/discovery_document.exs b/test/fixtures/http/vault/discovery_document.exs index b5e54bf..8890321 100644 --- a/test/fixtures/http/vault/discovery_document.exs +++ b/test/fixtures/http/vault/discovery_document.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "authorization_endpoint" => @@ -33,18 +33,6 @@ {"Cache-Control", "max-age=3600"}, {"Content-Type", "application/json"}, {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, - {"Date", "Sat, 12 Nov 2022 19:50:54 GMT"}, - {"Content-Length", "849"} - ], - request_url: - "http://127.0.0.1:8200/v1/identity/oidc/provider/default/.well-known/openid-configuration", - request: %HTTPoison.Request{ - method: :get, - url: - "http://127.0.0.1:8200/v1/identity/oidc/provider/default/.well-known/openid-configuration", - headers: [], - body: "", - params: %{}, - options: [] - } + {"Date", "Sat, 12 Nov 2022 19:50:54 GMT"} + ] } diff --git a/test/fixtures/http/vault/jwks.exs b/test/fixtures/http/vault/jwks.exs index 0edb01a..c11c772 100644 --- a/test/fixtures/http/vault/jwks.exs +++ b/test/fixtures/http/vault/jwks.exs @@ -1,4 +1,4 @@ -%HTTPoison.Response{ +%{ status_code: 200, body: %{ "keys" => [ @@ -26,16 +26,6 @@ {"Cache-Control", "no-store"}, {"Content-Type", "application/json"}, {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, - {"Date", "Sat, 12 Nov 2022 19:56:52 GMT"}, - {"Content-Length", "900"} - ], - request_url: "http://0.0.0.0:8200/v1/identity/oidc/provider/default/.well-known/keys", - request: %HTTPoison.Request{ - method: :get, - url: "http://0.0.0.0:8200/v1/identity/oidc/provider/default/.well-known/keys", - headers: [], - body: "", - params: %{}, - options: [] - } + {"Date", "Sat, 12 Nov 2022 19:56:52 GMT"} + ] } diff --git a/test/fixtures/jwks/jwk1.exs b/test/fixtures/jwks/jwk.exs similarity index 100% rename from test/fixtures/jwks/jwk1.exs rename to test/fixtures/jwks/jwk.exs diff --git a/test/fixtures/jwks/jwk2.exs b/test/fixtures/jwks/jwk2.exs deleted file mode 100644 index d4ccf70..0000000 --- a/test/fixtures/jwks/jwk2.exs +++ /dev/null @@ -1,11 +0,0 @@ -%{ - "alg" => "RS256", - "d" => - "ezKeapIKr6YkDoWKarXmj3dytjlS1_kuSHCQs9HLFftqR0MYIJICcBeXz2mmYUjkl0cpnce_xDCS2IOfV6KUpRhkNsOWHNtjqKXs-YHMapffgo_zeoZrdIdiDEqvm1X4D4qg8qbN2IGhhFsb8WO-aP3otSsSXreA7ibaDusxBNbxxWkaEwikBy61A1MTu5CR5GLYgsgQsZnawxJqDo1-QH6b-5E05XHD2PHG_xADLePC9zDsCr0kpCw1c-DuJ0SdKzV-OxoCbHuTRRP8Df7WztsLyFxYtDdZhgYOGmofPa_w8L6jLZTecixhBuSzV3OSGxua7eGd7MQDHf1vcpbh", - "e" => "AQAB", - "kid" => "example@dockyard.com", - "kty" => "RSA", - "n" => - "iUteZhwFt_wvc8QR5rfh7zShXqwlRMMwB-9kst-A2ixeUXrBkwqQrEoAy_FUOFgQTIb8gRFvzlc7oB6cWC-OFdt5XLQsBV6fTtnkEVZtVdze22V42qz3y0l5lztKGXJYjLSbB6kUF1SiT7wpbT7J-M9bSkxwdVWQYAMZsPg71IZ7qX4JyxcvUnqnXEseHMunsWcGGdqR6OcVQylAlKBi_biKSjbXVavWjXbk25-IDs6YQxNGu7RF-DZoDNYSyEFZWN9pY_wdPZy2RvgW_5okNjffcecg-HPMHRFHsKbIsHlX1XLJ7gbY79MtCTFQym4ZcrJqn42r99dw6242KoAB", - "use" => "sig" -} diff --git a/test/fixtures/jwks/jwks.exs b/test/fixtures/jwks/jwks.exs deleted file mode 100644 index 132969a..0000000 --- a/test/fixtures/jwks/jwks.exs +++ /dev/null @@ -1,26 +0,0 @@ -%{ - "keys" => [ - %{ - "alg" => "RS256", - "d" => - "X8TM24Zqbiha9geYYk_vZpANu16IadJLJLJ7ucTc3JaMbK8NCYNcHMoXKnNYPFxmq-UWAEIwh-2txOiOxuChVrblpfyE4SBJio1T0AUcCwmm8U6G-CsSHMMzWTt2dMTnArHjdyAIgOVRW5SVzhTTtaf4JY-47S-fbcJ7g0hmBbVih5i1sE2fad4I4qFHT-YFU_pnUHbteR6GQuRW4r03Eon8Aje6al2AxcYnfF8_cSOIOpkDgGavTtGYhhZPi2jZ7kPm6QGkNW5CyfEq5PGB6JOihw-XIFiiMzYgx052rnzoqALoLheXrI0By4kgHSmcqOOmq7aiOff45rlSbpsR", - "e" => "AQAB", - "kid" => "example@dockyard.com", - "kty" => "RSA", - "n" => - "qlKll8no4lPYXNSuTTnacpFHiXwPOv_htCYvIXmiR7CWhiiOHQqj7KWXIW7TGxyoLVIyeRM4mwvkLI-UgsSMYdEKTT0j7Ydjrr0zCunPu5Gxr2yOmcRaszAzGxJL5DwpA0V40RqMlm5OuwdqS4To_p9LlLxzMF6RZe1OqslV5RZ4Y8FmrWq6BV98eIziEHL0IKdsAIrrOYkkcLDdQeMNuTp_yNB8Xl2TdWSdsbRomrs2dCtCqZcXTsy2EXDceHvYhgAB33nh_w17WLrZQwMM-7kJk36Kk54jZd7i80AJf_s_plXn1mEh-L5IAL1vg3a9EOMFUl-lPiGqc3td_ykH", - "use" => "sig" - }, - %{ - "alg" => "RS256", - "d" => - "ezKeapIKr6YkDoWKarXmj3dytjlS1_kuSHCQs9HLFftqR0MYIJICcBeXz2mmYUjkl0cpnce_xDCS2IOfV6KUpRhkNsOWHNtjqKXs-YHMapffgo_zeoZrdIdiDEqvm1X4D4qg8qbN2IGhhFsb8WO-aP3otSsSXreA7ibaDusxBNbxxWkaEwikBy61A1MTu5CR5GLYgsgQsZnawxJqDo1-QH6b-5E05XHD2PHG_xADLePC9zDsCr0kpCw1c-DuJ0SdKzV-OxoCbHuTRRP8Df7WztsLyFxYtDdZhgYOGmofPa_w8L6jLZTecixhBuSzV3OSGxua7eGd7MQDHf1vcpbh", - "e" => "AQAB", - "kid" => "example@dockyard.com", - "kty" => "RSA", - "n" => - "iUteZhwFt_wvc8QR5rfh7zShXqwlRMMwB-9kst-A2ixeUXrBkwqQrEoAy_FUOFgQTIb8gRFvzlc7oB6cWC-OFdt5XLQsBV6fTtnkEVZtVdze22V42qz3y0l5lztKGXJYjLSbB6kUF1SiT7wpbT7J-M9bSkxwdVWQYAMZsPg71IZ7qX4JyxcvUnqnXEseHMunsWcGGdqR6OcVQylAlKBi_biKSjbXVavWjXbk25-IDs6YQxNGu7RF-DZoDNYSyEFZWN9pY_wdPZy2RvgW_5okNjffcecg-HPMHRFHsKbIsHlX1XLJ7gbY79MtCTFQym4ZcrJqn42r99dw6242KoAB", - "use" => "sig" - } - ] -} diff --git a/test/openid_connect/document/cache_test.exs b/test/openid_connect/document/cache_test.exs new file mode 100644 index 0000000..734ecaf --- /dev/null +++ b/test/openid_connect/document/cache_test.exs @@ -0,0 +1,139 @@ +defmodule OpenIDConnect.Document.CacheTest do + use ExUnit.Case, async: true + import OpenIDConnect.Document.Cache + + @valid_document %OpenIDConnect.Document{ + authorization_endpoint: "https://common.auth0.com/authorize", + claims_supported: [ + "aud", + "auth_time", + "created_at", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "identities", + "iss", + "name", + "nickname", + "phone_number", + "picture", + "sub" + ], + end_session_endpoint: nil, + expires_at: DateTime.utc_now(), + jwks: %JOSE.JWK{}, + raw: "", + response_types_supported: [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "id_token token", + "code id_token token" + ], + token_endpoint: "https://common.auth0.com/oauth/token" + } + + describe "put/2" do + test "persists a documents to the cache" do + uri = uniq_uri() + document = %{@valid_document | expires_at: DateTime.utc_now() |> DateTime.add(60, :second)} + + put(uri, document) + + assert %{^uri => {_ref, _last_fetched_at, ^document}} = flush() + end + + test "does not persist expired documents" do + uri = uniq_uri() + document = %{@valid_document | expires_at: DateTime.utc_now() |> DateTime.add(-60, :second)} + + put(uri, document) + + refute Map.has_key?(flush(), uri) + end + + test "schedules document removal and removes it once it's expired" do + uri = uniq_uri() + document = %{@valid_document | expires_at: DateTime.utc_now() |> DateTime.add(60, :second)} + + put(uri, document) + + assert %{^uri => {ref, _last_fetched_at, _document}} = flush() + assert Process.read_timer(ref) in 58_000..62_000 + + send(OpenIDConnect.Document.Cache, {:remove, uri}) + refute Map.has_key?(flush(), uri) + end + end + + describe "fetch/1" do + test "returns error when there is no cache" do + uri = uniq_uri() + assert fetch(uri) == :error + end + + test "returns cached documents" do + uri = uniq_uri() + document = %{@valid_document | expires_at: DateTime.utc_now() |> DateTime.add(60, :second)} + put(uri, document) + + assert {:ok, cached_document} = fetch(uri) + assert document == cached_document + end + + test "does not return documents that already expired" do + uri = uniq_uri() + now = DateTime.utc_now() + document = %{@valid_document | expires_at: DateTime.add(now, -1, :second)} + state = %{uri => {nil, now, document}} + + assert handle_call({:fetch, uri}, self(), state) == {:reply, :error, %{}} + end + end + + describe ":gc" do + test "doesn't do anything when cache is empty" do + {:ok, pid} = start_link(name: :gc_test) + assert Enum.count(flush(pid)) == 0 + send(pid, :gc) + assert flush(pid) == %{} + end + + test "removes excessive entries from cache" do + {:ok, pid} = start_link(name: :gc_test) + + documents = + for i <- 1..2000 do + expires_at = DateTime.utc_now() |> DateTime.add(60 + i, :second) + document = %{@valid_document | expires_at: expires_at} + put(pid, uniq_uri(), document) + document + end + + assert Enum.count(flush(pid)) == 2000 + + send(pid, :gc) + + assert state = flush(pid) + assert Enum.count(state) == 1000 + + {_uri, {_ref, _last_fetched_at, document}} = + Enum.min_by( + state, + fn {_uri, {_ref, last_fetched_at, _document}} -> + last_fetched_at + end, + DateTime + ) + + assert document.expires_at == Enum.at(documents, 1000).expires_at + end + end + + defp uniq_uri, do: "http://example.com:#{System.unique_integer([:positive])}" +end diff --git a/test/openid_connect/document_test.exs b/test/openid_connect/document_test.exs new file mode 100644 index 0000000..cb9bb34 --- /dev/null +++ b/test/openid_connect/document_test.exs @@ -0,0 +1,195 @@ +defmodule OpenIDConnect.DocumentTest do + use ExUnit.Case, async: true + import OpenIDConnect.Fixtures + import OpenIDConnect.Document + + describe "fetch_document/1" do + test "returns valid document from a given url" do + {_bypass, uri} = start_fixture("auth0") + + assert {:ok, document} = fetch_document(uri) + + assert %OpenIDConnect.Document{ + authorization_endpoint: "https://common.auth0.com/authorize", + claims_supported: [ + "aud", + "auth_time", + "created_at", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "identities", + "iss", + "name", + "nickname", + "phone_number", + "picture", + "sub" + ], + end_session_endpoint: nil, + expires_at: expires_at, + jwks: %JOSE.JWK{}, + raw: _json, + response_types_supported: [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "id_token token", + "code id_token token" + ], + token_endpoint: "https://common.auth0.com/oauth/token" + } = document + + assert DateTime.diff(expires_at, DateTime.utc_now()) in (60 * 60 - 10)..(60 * 60 + 10) + end + + test "supports all gateway providers" do + for provider <- ["auth0", "azure", "google", "keycloak", "okta", "onelogin", "vault"] do + {_bypass, uri} = start_fixture(provider) + assert {:ok, document} = fetch_document(uri) + assert not is_nil(document.jwks) + end + end + + test "caches the document" do + {_bypass, uri} = start_fixture("auth0") + + assert {:ok, document} = fetch_document(uri) + assert {:ok, ^document} = fetch_document(uri) + end + + test "handles non 2XX response codes" do + bypass = Bypass.open() + + Bypass.expect_once(bypass, "GET", "/.well-known/discovery-document.json", fn conn -> + Plug.Conn.resp(conn, 401, "{}") + end) + + uri = "http://localhost:#{bypass.port}/.well-known/discovery-document.json" + + assert fetch_document(uri) == {:error, {401, "{}"}} + end + + test "handles invalid responses" do + bypass = Bypass.open() + + Bypass.expect_once(bypass, "GET", "/.well-known/discovery-document.json", fn conn -> + Plug.Conn.resp(conn, 200, "{}") + end) + + uri = "http://localhost:#{bypass.port}/.well-known/discovery-document.json" + + assert fetch_document(uri) == {:error, :invalid_document} + end + + test "handles response errors" do + bypass = Bypass.open() + uri = "http://localhost:#{bypass.port}/.well-known/discovery-document.json" + Bypass.down(bypass) + + assert fetch_document(uri) == {:error, %Mint.TransportError{reason: :econnrefused}} + end + + test "takes expiration date from Cache-Control headers of the discovery document" do + bypass = Bypass.open() + endpoint = "http://localhost:#{bypass.port}/" + provider = "vault" + + Bypass.expect_once(bypass, "GET", "/.well-known/jwks.json", fn conn -> + {status_code, body, headers} = load_fixture(provider, "jwks") + send_response(conn, status_code, body, headers) + end) + + Bypass.expect_once(bypass, "GET", "/.well-known/discovery-document.json", fn conn -> + {status_code, body, headers} = load_fixture(provider, "discovery_document") + body = Map.merge(body, %{"jwks_uri" => "#{endpoint}.well-known/jwks.json"}) + + headers = + for {k, v} <- headers, + k = String.downcase(k), + k not in ["cache-control", "age"] do + {k, v} + end + + headers = headers ++ [{"cache-control", "max-age=300"}] + send_response(conn, status_code, body, headers) + end) + + uri = "#{endpoint}.well-known/discovery-document.json" + + assert {:ok, document} = fetch_document(uri) + expected_expires_at = DateTime.add(DateTime.utc_now(), 300, :second) + assert DateTime.diff(document.expires_at, expected_expires_at) in -3..3 + end + + test "takes expiration date from Cache-Control and Age headers of the discovery document" do + bypass = Bypass.open() + endpoint = "http://localhost:#{bypass.port}/" + provider = "vault" + + Bypass.expect_once(bypass, "GET", "/.well-known/jwks.json", fn conn -> + {status_code, body, headers} = load_fixture(provider, "jwks") + send_response(conn, status_code, body, headers) + end) + + Bypass.expect_once(bypass, "GET", "/.well-known/discovery-document.json", fn conn -> + {status_code, body, headers} = load_fixture(provider, "discovery_document") + body = Map.merge(body, %{"jwks_uri" => "#{endpoint}.well-known/jwks.json"}) + + headers = + for {k, v} <- headers, + k = String.downcase(k), + k not in ["cache-control", "age"] do + {k, v} + end + + headers = headers ++ [{"cache-control", "max-age=300"}, {"age", "100"}] + send_response(conn, status_code, body, headers) + end) + + uri = "#{endpoint}.well-known/discovery-document.json" + + assert {:ok, document} = fetch_document(uri) + expected_expires_at = DateTime.add(DateTime.utc_now(), 300 - 100, :second) + assert DateTime.diff(document.expires_at, expected_expires_at) in -3..3 + end + + test "takes expiration date from Cache-Control and Age headers of the jwks document" do + bypass = Bypass.open() + endpoint = "http://localhost:#{bypass.port}/" + provider = "vault" + + Bypass.expect_once(bypass, "GET", "/.well-known/jwks.json", fn conn -> + {status_code, body, headers} = load_fixture(provider, "jwks") + + headers = + for {k, v} <- headers, + k = String.downcase(k), + k not in ["cache-control", "age"] do + {k, v} + end + + headers = headers ++ [{"cache-control", "max-age=300"}, {"age", "100"}] + send_response(conn, status_code, body, headers) + end) + + Bypass.expect_once(bypass, "GET", "/.well-known/discovery-document.json", fn conn -> + {status_code, body, headers} = load_fixture(provider, "discovery_document") + body = Map.merge(body, %{"jwks_uri" => "#{endpoint}.well-known/jwks.json"}) + + send_response(conn, status_code, body, headers) + end) + + uri = "#{endpoint}.well-known/discovery-document.json" + + assert {:ok, document} = fetch_document(uri) + expected_expires_at = DateTime.add(DateTime.utc_now(), 300 - 100, :second) + assert DateTime.diff(document.expires_at, expected_expires_at) in -3..3 + end + end +end diff --git a/test/openid_connect/worker_test.exs b/test/openid_connect/worker_test.exs deleted file mode 100644 index 1732d54..0000000 --- a/test/openid_connect/worker_test.exs +++ /dev/null @@ -1,125 +0,0 @@ -defmodule OpenIDConnect.WorkerTest do - use ExUnit.Case - import Mox - - setup :set_mox_global - setup :verify_on_exit! - - @google_document Fixtures.load("google", :discovery_document) - @google_certs Fixtures.load("google", :jwks) - - alias OpenIDConnect.HTTPClientMock - - test "starting with :ignore creates a worker with empty state" do - {:ok, _pid} = OpenIDConnect.Worker.start_link(:ignore) - end - - test "starting with a single provider will retrieve the necessary documents" do - mock_http_requests() - - config = Application.get_env(:openid_connect, :providers) - - {:ok, pid} = start_supervised({OpenIDConnect.Worker, config}) - - state = :sys.get_state(pid) - - expected_document = - @google_document - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> OpenIDConnect.normalize_discovery_document() - - expected_jwk = - @google_certs - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> JOSE.JWK.from() - - assert expected_document == get_in(state, ["google", :documents, :discovery_document]) - assert expected_jwk == get_in(state, ["google", :documents, :jwk]) - end - - test "worker can respond to a call for the config using string name" do - mock_http_requests() - - config = Application.get_env(:openid_connect, :providers) - - {:ok, pid} = start_supervised({OpenIDConnect.Worker, config}) - - google_config = GenServer.call(pid, {:config, "google"}) - - assert List.keyfind(config, "google", 0) == {"google", google_config} - end - - test "worker can respond to a call for the config using atom name" do - mock_http_requests() - - config = Application.get_env(:openid_connect, :providers) - - {:ok, pid} = start_supervised({OpenIDConnect.Worker, config}) - - google_config = GenServer.call(pid, {:config, :google2}) - - assert List.keyfind(config, :google2, 0) == {:google2, google_config} - end - - test "worker can respond to a call for a provider's discovery document" do - mock_http_requests() - - config = Application.get_env(:openid_connect, :providers) - - {:ok, pid} = start_supervised({OpenIDConnect.Worker, config}) - - discovery_document = GenServer.call(pid, {:discovery_document, "google"}) - - expected_document = - @google_document - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> OpenIDConnect.normalize_discovery_document() - - assert expected_document == discovery_document - end - - test "worker can respond to a call for a provider's jwk" do - mock_http_requests() - - config = Application.get_env(:openid_connect, :providers) - - {:ok, pid} = start_supervised({OpenIDConnect.Worker, config}) - - jwk = GenServer.call(pid, {:jwk, "google"}) - - expected_jwk = - @google_certs - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> JOSE.JWK.from() - - assert expected_jwk == jwk - end - - defp mock_http_requests do - HTTPClientMock - |> expect(:get, fn "https://accounts.google.com/.well-known/openid-configuration", - _headers, - _opts -> - @google_document - end) - |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, _opts -> - @google_certs - end) - |> expect(:get, fn "https://accounts.google.com/.well-known/openid-configuration", - _headers, - _opts -> - @google_document - end) - |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, _opts -> - @google_certs - end) - end -end diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index 0d4665e..c95bd0c 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -1,554 +1,332 @@ defmodule OpenIDConnectTest do use ExUnit.Case - import Mox - - setup :set_mox_global - setup :verify_on_exit! - setup :set_jose_json_lib - - @google_document Fixtures.load("google", :discovery_document) - @google_certs Fixtures.load("google", :jwks) - - alias OpenIDConnect.{HTTPClientMock, MockWorker} - - # XXX: Unskip this test when we're back on Hex - @tag :skip - test "README install version check" do - app = :openid_connect - - app_version = "#{Application.spec(app, :vsn)}" - readme = File.read!("README.md") - [_, readme_versions] = Regex.run(~r/{:#{app}, "(.+)"}/, readme) - - assert Version.match?( - app_version, - readme_versions - ), - """ - Install version constraint in README.md does not match to current app version. - Current App Version: #{app_version} - Readme Install Versions: #{readme_versions} - """ - end - - describe "update_documents" do - test "when the new documents are retrieved successfully" do - config = [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration" - ] - - HTTPClientMock - |> expect(:get, fn "https://accounts.google.com/.well-known/openid-configuration", - _headers, - _opts -> - @google_document - end) - |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, _opts -> - @google_certs - end) - - expected_document = - @google_document - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> OpenIDConnect.normalize_discovery_document() - - expected_jwk = - @google_certs - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> JOSE.JWK.from() - - {:ok, - %{ - discovery_document: discovery_document, - jwk: jwk, - remaining_lifetime: remaining_lifetime - }} = OpenIDConnect.update_documents(config) - - assert expected_document == discovery_document - assert expected_jwk == jwk - assert remaining_lifetime == 21_527 + import OpenIDConnect.Fixtures + import OpenIDConnect + + @config %{ + discovery_document_uri: nil, + client_id: "CLIENT_ID", + client_secret: "CLIENT_SECRET", + redirect_uri: "https://localhost/redirect_uri", + response_type: "code id_token token", + scope: "openid email profile" + } + + describe "authorization_uri/2" do + test "generates authorization url" do + {_bypass, uri} = start_fixture("google") + config = %{@config | discovery_document_uri: uri} + + assert authorization_uri(config) == + {:ok, + "https://accounts.google.com/o/oauth2/v2/auth?" <> + "client_id=CLIENT_ID" <> + "&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <> + "&response_type=code+id_token+token" <> + "&scope=openid+email+profile"} end - test "fails during open id configuration document with HTTPoison error" do - config = [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration" - ] - - expect( - HTTPClientMock, - :get, - fn "https://accounts.google.com/.well-known/openid-configuration", _headers, _opts -> - {:ok, %HTTPoison.Error{id: nil, reason: :nxdomain}} - end - ) - - assert OpenIDConnect.update_documents(config) == - {:error, :update_documents, %HTTPoison.Error{id: nil, reason: :nxdomain}} + test "adds optional params" do + {_bypass, uri} = start_fixture("google") + config = %{@config | discovery_document_uri: uri} + + assert authorization_uri(config, %{"state" => "foo"}) == + {:ok, + "https://accounts.google.com/o/oauth2/v2/auth?" <> + "client_id=CLIENT_ID" <> + "&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <> + "&response_type=code+id_token+token" <> + "&scope=openid+email+profile" <> + "&state=foo"} end - test "non-200 response for open id configuration document" do - config = [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration" - ] - - expect( - HTTPClientMock, - :get, - fn "https://accounts.google.com/.well-known/openid-configuration", _headers, _opts -> - {:ok, %HTTPoison.Response{status_code: 404}} - end - ) - - assert OpenIDConnect.update_documents(config) == - {:error, :update_documents, %HTTPoison.Response{status_code: 404}} + test "params can override default values" do + {_bypass, uri} = start_fixture("google") + config = %{@config | discovery_document_uri: uri} + + assert authorization_uri(config, %{client_id: "foo"}) == + {:ok, + "https://accounts.google.com/o/oauth2/v2/auth?" <> + "client_id=foo" <> + "&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <> + "&response_type=code+id_token+token" <> + "&scope=openid+email+profile"} end - test "fails during certs with HTTPoison error" do - config = [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration" - ] + test "returns error when document is not available" do + bypass = Bypass.open() + uri = "http://localhost:#{bypass.port}/.well-known/discovery-document.json" + Bypass.down(bypass) - HTTPClientMock - |> expect(:get, fn "https://accounts.google.com/.well-known/openid-configuration", - _headers, - _opts -> - @google_document - end) - |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, _opts -> - {:ok, %HTTPoison.Error{reason: :nxdomain}} - end) + config = %{@config | discovery_document_uri: uri} - assert OpenIDConnect.update_documents(config) == - {:error, :update_documents, %HTTPoison.Error{id: nil, reason: :nxdomain}} + assert authorization_uri(config, %{client_id: "foo"}) == + {:error, %Mint.TransportError{reason: :econnrefused}} end + end - test "non-200 response for certs" do - config = [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration" - ] - - HTTPClientMock - |> expect(:get, fn "https://accounts.google.com/.well-known/openid-configuration", - _headers, - _opts -> - @google_document - end) - |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, _opts -> - {:ok, %HTTPoison.Response{status_code: 404}} - end) + describe "end_session_uri/2" do + test "returns error when provider doesn't specify end_session_endpoint" do + {_bypass, uri} = start_fixture("google") + config = %{@config | discovery_document_uri: uri} - assert OpenIDConnect.update_documents(config) == - {:error, :update_documents, %HTTPoison.Response{status_code: 404}} + assert end_session_uri(config) == {:error, :endpoint_not_set} end - test "with HTTP client options" do - config = [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration" - ] - - opts = [ssl: [{:verify, :verify_none}]] - Application.put_env(:openid_connect, :http_client_options, opts) - - HTTPClientMock - |> expect(:get, fn - "https://accounts.google.com/.well-known/openid-configuration", _headers, ^opts -> - @google_document - end) - |> expect(:get, fn "https://www.googleapis.com/oauth2/v3/certs", _headers, ^opts -> - {:ok, %HTTPoison.Response{status_code: 404}} - end) + test "generates authorization url" do + {_bypass, uri} = start_fixture("okta") + config = %{@config | discovery_document_uri: uri} - assert OpenIDConnect.update_documents(config) == - {:error, :update_documents, %HTTPoison.Response{status_code: 404}} + assert end_session_uri(config) == + {:ok, "https://common.okta.com/oauth2/v1/logout?client_id=CLIENT_ID"} end - end - describe "normalize_discovery_document" do - test "defaults to empty list if claims_supported is missing" do - document_without_claims = - @google_document - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> Map.delete("claims_supported") - - normalized_claims = - document_without_claims - |> OpenIDConnect.normalize_discovery_document() - |> Map.get("claims_supported") - - assert normalized_claims == [] - end - end + test "adds optional params" do + {_bypass, uri} = start_fixture("okta") + config = %{@config | discovery_document_uri: uri} - describe "generating the authorization uri" do - test "with default worker name" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + assert end_session_uri(config, %{"state" => "foo"}) == + {:ok, "https://common.okta.com/oauth2/v1/logout?client_id=CLIENT_ID&state=foo"} + end - try do - expected = - "https://accounts.google.com/o/oauth2/v2/auth?client_id=CLIENT_ID_1&redirect_uri=https%3A%2F%2Fdev.example.com%3A4200%2Fsession&response_type=code+id_token+token&scope=openid+email+profile" + test "params can override default values" do + {_bypass, uri} = start_fixture("okta") + config = %{@config | discovery_document_uri: uri} - assert OpenIDConnect.authorization_uri("google") == expected - after - GenServer.stop(pid) - end + assert end_session_uri(config, %{client_id: "foo"}) == + {:ok, "https://common.okta.com/oauth2/v1/logout?client_id=foo"} end - test "with optional params" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + test "returns error when document is not available" do + bypass = Bypass.open() + uri = "http://localhost:#{bypass.port}/.well-known/discovery-document.json" + Bypass.down(bypass) - try do - expected = - "https://accounts.google.com/o/oauth2/v2/auth?client_id=CLIENT_ID_1&redirect_uri=https%3A%2F%2Fdev.example.com%3A4200%2Fsession&response_type=code+id_token+token&scope=openid+email+profile&hd=dockyard.com" + config = %{@config | discovery_document_uri: uri} - assert OpenIDConnect.authorization_uri("google", %{"hd" => "dockyard.com"}) == expected - after - GenServer.stop(pid) - end + assert end_session_uri(config, %{client_id: "foo"}) == + {:error, %Mint.TransportError{reason: :econnrefused}} end + end - test "with overridden params" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + describe "fetch_tokens/2" do + test "fetches the token from OAuth token endpoint" do + bypass = Bypass.open() + test_pid = self() - try do - expected = - "https://accounts.google.com/o/oauth2/v2/auth?client_id=CLIENT_ID_1&redirect_uri=https%3A%2F%2Fdev.example.com%3A4200%2Fsession&response_type=code+id_token+token&scope=something+else" + token_response_attrs = %{ + "access_token" => "ACCESS_TOKEN", + "id_token" => "ID_TOKEN", + "refresh_token" => "REFRESH_TOKEN" + } - assert OpenIDConnect.authorization_uri("google", %{scope: "something else"}) == expected - after - GenServer.stop(pid) - end - end + Bypass.expect_once(bypass, "POST", "/token", fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + send(test_pid, {:req, body}) + Plug.Conn.resp(conn, 200, Jason.encode!(token_response_attrs)) + end) - test "with custom worker name" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :other_openid_worker) + token_endpoint = "http://localhost:#{bypass.port}/token" + {_bypass, uri} = start_fixture("google", %{token_endpoint: token_endpoint}) + config = %{@config | discovery_document_uri: uri} - try do - expected = - "https://accounts.google.com/o/oauth2/v2/auth?client_id=CLIENT_ID_1&redirect_uri=https%3A%2F%2Fdev.example.com%3A4200%2Fsession&response_type=code+id_token+token&scope=openid+email+profile" + assert fetch_tokens(config, %{code: "1234", id_token: "abcd"}) == + {:ok, token_response_attrs} - assert OpenIDConnect.authorization_uri("google", %{}, :other_openid_worker) == expected - after - GenServer.stop(pid) - end - end - end + assert_receive {:req, body} - describe "fetching tokens" do - test "when token fetch is successful" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) - - config = GenServer.call(:openid_connect, {:config, "google"}) - - form_body = [ - client_id: config[:client_id], - client_secret: config[:client_secret], - code: "1234", - grant_type: "authorization_code", - redirect_uri: config[:redirect_uri] - ] - - try do - expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", - {:form, ^form_body}, - _headers, - _opts -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{})}} - end) - - {:ok, body} = OpenIDConnect.fetch_tokens("google", %{code: "1234"}) - - assert body == %{} - after - GenServer.stop(pid) - end + assert body == + "client_id=CLIENT_ID" <> + "&client_secret=CLIENT_SECRET" <> + "&code=1234" <> + "&grant_type=authorization_code" <> + "&id_token=abcd" <> + "&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" end - test "when token fetch is successful with a different GenServer name" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :other_openid_connect) - - config = GenServer.call(:other_openid_connect, {:config, "google"}) - - form_body = [ - client_id: config[:client_id], - client_secret: config[:client_secret], - code: "1234", - grant_type: "authorization_code", - id_token: "abcd", - redirect_uri: config[:redirect_uri] - ] - - try do - expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", - {:form, ^form_body}, - _headers, - _opts -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{})}} - end) - - {:ok, body} = - OpenIDConnect.fetch_tokens( - "google", - %{code: "1234", id_token: "abcd"}, - :other_openid_connect - ) - - assert body == %{} - after - GenServer.stop(pid) - end - end + test "allows to override the default params" do + bypass = Bypass.open() + test_pid = self() - test "when params are overridden" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + token_response_attrs = %{ + "access_token" => "ACCESS_TOKEN", + "id_token" => "ID_TOKEN", + "refresh_token" => "REFRESH_TOKEN" + } - config = GenServer.call(:openid_connect, {:config, "google"}) + Bypass.expect_once(bypass, "POST", "/token", fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + send(test_pid, {:req, body}) + Plug.Conn.resp(conn, 200, Jason.encode!(token_response_attrs)) + end) - form_body = [ - client_id: config[:client_id], - client_secret: config[:client_secret], - grant_type: "refresh_token", - redirect_uri: config[:redirect_uri] - ] + token_endpoint = "http://localhost:#{bypass.port}/token" + {_bypass, uri} = start_fixture("google", %{token_endpoint: token_endpoint}) + config = %{@config | discovery_document_uri: uri} - try do - expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", - {:form, ^form_body}, - _headers, - _opts -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{})}} - end) + fetch_tokens(config, %{client_id: "foo"}) - {:ok, body} = OpenIDConnect.fetch_tokens("google", %{grant_type: "refresh_token"}) + assert_receive {:req, body} - assert body == %{} - after - GenServer.stop(pid) - end + assert body == + "client_id=foo" <> + "&client_secret=CLIENT_SECRET" <> + "&grant_type=authorization_code" <> + "&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" end - test "when token fetch fails with bad domain" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) - - http_error = %HTTPoison.Error{reason: :nxdomain} + test "returns error when token endpoint is not available" do + bypass = Bypass.open() + Bypass.down(bypass) + token_endpoint = "http://localhost:#{bypass.port}/token" + {_bypass, uri} = start_fixture("google", %{token_endpoint: token_endpoint}) + config = %{@config | discovery_document_uri: uri} - try do - expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", - {:form, _form_body}, - _headers, - _opts -> - {:ok, http_error} - end) - - resp = OpenIDConnect.fetch_tokens("google", %{code: "1234"}) - - assert resp == {:error, :fetch_tokens, http_error} - after - GenServer.stop(pid) - end + assert fetch_tokens(config, %{client_id: "foo"}) == + {:error, %Mint.TransportError{reason: :econnrefused}} end - test "when token fetch doesn't return a 200 response" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + test "returns error when token endpoint is responds with non 2XX status code" do + bypass = Bypass.open() - http_error = %HTTPoison.Response{status_code: 404} + Bypass.expect_once(bypass, "POST", "/token", fn conn -> + Plug.Conn.resp(conn, 401, Jason.encode!(%{"error" => "unauthorized"})) + end) - try do - expect(HTTPClientMock, :post, fn "https://oauth2.googleapis.com/token", - {:form, _form_body}, - _headers, - _opts -> - {:ok, http_error} - end) + token_endpoint = "http://localhost:#{bypass.port}/token" + {_bypass, uri} = start_fixture("google", %{token_endpoint: token_endpoint}) + config = %{@config | discovery_document_uri: uri} - resp = OpenIDConnect.fetch_tokens("google", %{code: "1234"}) + assert fetch_tokens(config, %{client_id: "foo"}) == + {:error, {401, "{\"error\":\"unauthorized\"}"}} + end - assert resp == {:error, :fetch_tokens, http_error} - after - GenServer.stop(pid) + test "returns error when real provider token endpoint is responded with invalid code" do + {_bypass, uri} = start_fixture("google") + config = %{@config | discovery_document_uri: uri} + assert {:error, {401, resp}} = fetch_tokens(config, %{code: "foo"}) + resp_json = Jason.decode!(resp) + + assert resp_json == %{ + "error" => "invalid_client", + "error_description" => "The OAuth client was not found." + } + + for provider <- ["auth0", "okta", "onelogin"] do + {_bypass, uri} = start_fixture(provider) + config = %{@config | discovery_document_uri: uri} + assert {:error, {status, _resp}} = fetch_tokens(config, %{code: "foo"}) + assert status in 400..499 end end - end - - describe "jwt verification" do - test "is successful" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) - - try do - {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") - :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) - claims = %{"email" => "brian@example.com"} + test "returns error when document is not available" do + bypass = Bypass.open() + uri = "http://localhost:#{bypass.port}/.well-known/discovery-document.json" + Bypass.down(bypass) - {_alg, token} = - jwk - |> JOSE.JWK.from() - |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) - |> JOSE.JWS.compact() + config = %{@config | discovery_document_uri: uri} - result = OpenIDConnect.verify("google", token) - assert result == {:ok, claims} - after - GenServer.stop(pid) - end + assert fetch_tokens(config, %{code: "foo"}) == + {:error, %Mint.TransportError{reason: :econnrefused}} end + end - test "is successful with multiple jwks" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + describe "verify/2" do + test "returns error when token has invalid format" do + assert verify(@config, "foo") == + {:error, {:invalid_jwt, "invalid token format"}} + end - try do - {jwk, []} = Code.eval_file("test/fixtures/jwks/jwks.exs") - :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) + test "returns error when encoded token is not a JSON map" do + token = + ["fail", "fail", "fail"] + |> Enum.map_join(".", fn header -> Base.encode64(header) end) - claims = %{"email" => "brian@example.com"} + assert verify(@config, token) == + {:error, {:invalid_jwt, "token claims did not contain a JSON payload"}} + end - {_alg, token} = - jwk - |> Map.get("keys") - |> List.last() - |> JOSE.JWK.from() - |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) - |> JOSE.JWS.compact() + test "returns error when encoded token is doesn't have valid 'alg'" do + token = + ["{}", "{}", "{}"] + |> Enum.map_join(".", fn header -> Base.encode64(header) end) - result = OpenIDConnect.verify("google", token) - assert result == {:ok, claims} - after - GenServer.stop(pid) - end + assert verify(@config, token) == + {:error, {:invalid_jwt, "no `alg` found in token"}} end - test "fails with invalid token format" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) - - try do - {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") - :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) + test "returns error when token is valid but invalid for a provider" do + {_bypass, uri} = start_fixture("okta") + config = %{@config | discovery_document_uri: uri} + {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") - result = OpenIDConnect.verify("google", "fail") - assert result == {:error, :verify, "invalid token format"} - after - GenServer.stop(pid) - end - end + claims = %{"email" => "brian@example.com"} - test "fails with invalid token claims format" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) - - try do - {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") - :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) - - token = - [ - "fail", - "fail", - "fail" - ] - |> Enum.map_join(".", fn header -> Base.encode64(header) end) - - result = OpenIDConnect.verify("google", token) - assert result == {:error, :verify, "token claims did not contain a JSON payload"} - after - GenServer.stop(pid) - end - end + {_alg, token} = + jwk + |> JOSE.JWK.from() + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() - test "fails with token not including algorithm hint" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) - - try do - {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") - :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) - - token = - [ - "{}", - "{}", - "{}" - ] - |> Enum.map_join(".", fn header -> Base.encode64(header) end) - - result = OpenIDConnect.verify("google", token) - assert result == {:error, :verify, "no `alg` found in token"} - after - GenServer.stop(pid) - end + assert verify(config, token) == {:error, {:invalid_jwt, "verification failed"}} end - test "fails when verification fails" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + test "returns claims when encoded token is valid" do + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") + jwk = JOSE.JWK.from(jwks) + {_, jwk_pubkey} = JOSE.JWK.to_public_map(jwk) - try do - {jwk1, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") - {jwk2, []} = Code.eval_file("test/fixtures/jwks/jwk2.exs") - :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk1)}) + {_bypass, uri} = start_fixture("vault", %{"jwks" => jwk_pubkey}) + config = %{@config | discovery_document_uri: uri} - claims = %{"email" => "brian@example.com"} + claims = %{"email" => "brian@example.com"} - {_alg, token} = - jwk2 - |> JOSE.JWK.from() - |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) - |> JOSE.JWS.compact() + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() - result = OpenIDConnect.verify("google", token) - assert result == {:error, :verify, "verification failed"} - after - GenServer.stop(pid) - end + assert verify(config, token) == {:ok, claims} end - test "fails when verification fails due to token manipulation" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + test "returns error when token is altered" do + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") + jwk = JOSE.JWK.from(jwks) + {_, jwk_pubkey} = JOSE.JWK.to_public_map(jwk) - try do - {jwk, []} = Code.eval_file("test/fixtures/jwks/jwk1.exs") - :ok = GenServer.call(pid, {:put, :jwk, JOSE.JWK.from(jwk)}) + {_bypass, uri} = start_fixture("vault", %{"jwks" => jwk_pubkey}) + config = %{@config | discovery_document_uri: uri} - claims = %{"email" => "brian@example.com"} + claims = %{"email" => "brian@example.com"} - {_alg, token} = - jwk - |> JOSE.JWK.from() - |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) - |> JOSE.JWS.compact() + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() - result = OpenIDConnect.verify("google", token <> " :)") - assert result == {:error, :verify, "verification error"} - after - GenServer.stop(pid) - end + assert verify(config, token <> ":)") == {:error, {:invalid_jwt, "verification failed"}} end - end - describe "reconfigure/2" do - test "updates provider configs" do - {:ok, pid} = GenServer.start_link(MockWorker, [], name: :openid_connect) + test "returns error when document is not available" do + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") - vault_document = - Fixtures.load(:vault, :discovery_document) - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> OpenIDConnect.normalize_discovery_document() + bypass = Bypass.open() + uri = "http://localhost:#{bypass.port}/.well-known/discovery-document.json" + Bypass.down(bypass) - state = %{ - document: vault_document - } + config = %{@config | discovery_document_uri: uri} - OpenIDConnect.reconfigure(state) + claims = %{"email" => "brian@example.com"} - assert :sys.get_state(pid) == state - end - end + {_alg, token} = + jwks + |> JOSE.JWK.from() + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() - defp set_jose_json_lib(_) do - JOSE.json_module(JasonEncoder) - [] + assert verify(config, token) == + {:error, %Mint.TransportError{reason: :econnrefused}} + end end end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex deleted file mode 100644 index 41447b0..0000000 --- a/test/support/fixtures.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Fixtures do - @moduledoc """ - Helpers for loading fixtures. - - TODO: Consider adding a mix task to rehydrate this from provided - discovery_document_uris. - """ - - def load(provider, type) do - response = - Code.eval_file("test/fixtures/http/#{provider}/#{type}.exs") - |> elem(0) - |> serialize() - - {:ok, response} - end - - defp serialize(%HTTPoison.Response{body: body} = response), - do: %{response | body: Jason.encode!(body)} - - defp serialize(response), do: response -end diff --git a/test/support/fixutes.ex b/test/support/fixutes.ex new file mode 100644 index 0000000..392cf98 --- /dev/null +++ b/test/support/fixutes.ex @@ -0,0 +1,37 @@ +defmodule OpenIDConnect.Fixtures do + def start_fixture(provider, overrides \\ %{}) do + bypass = Bypass.open() + endpoint = "http://localhost:#{bypass.port}/" + {jwks, overrides} = Map.pop(overrides, "jwks") + + Bypass.expect_once(bypass, "GET", "/.well-known/jwks.json", fn conn -> + {status_code, body, headers} = load_fixture(provider, "jwks") + body = if jwks, do: jwks, else: body + send_response(conn, status_code, body, headers) + end) + + Bypass.expect_once(bypass, "GET", "/.well-known/discovery-document.json", fn conn -> + {status_code, body, headers} = load_fixture(provider, "discovery_document") + body = Map.merge(body, %{"jwks_uri" => "#{endpoint}.well-known/jwks.json"}) + body = Map.merge(body, overrides) + send_response(conn, status_code, body, headers) + end) + + {bypass, "#{endpoint}.well-known/discovery-document.json"} + end + + def load_fixture(provider, type) do + {%{status_code: status_code, body: body, headers: headers}, _} = + Code.eval_file("test/fixtures/http/#{provider}/#{type}.exs") + + {status_code, body, headers} + end + + def send_response(conn, status_code, body, headers) do + headers + |> Enum.reduce(conn, fn {key, value}, conn -> + Plug.Conn.put_resp_header(conn, key, value) + end) + |> Plug.Conn.resp(status_code, Jason.encode!(body)) + end +end diff --git a/test/support/jason_encoder.ex b/test/support/jason_encoder.ex deleted file mode 100644 index d29319b..0000000 --- a/test/support/jason_encoder.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule JasonEncoder do - @moduledoc """ - Convenience module to pass to JOSE - """ - - def encode(term) do - Jason.encode!(term) - end - - def decode(string) do - Jason.decode!(string) - end -end diff --git a/test/support/mock_worker.ex b/test/support/mock_worker.ex deleted file mode 100644 index bbf622d..0000000 --- a/test/support/mock_worker.ex +++ /dev/null @@ -1,58 +0,0 @@ -defmodule OpenIDConnect.MockWorker do - @moduledoc """ - Mock the Worker. - """ - use GenServer - - @google_document Fixtures.load(:google, :discovery_document) - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> OpenIDConnect.normalize_discovery_document() - - @google_jwk Fixtures.load(:google, :jwks) - |> elem(1) - |> Map.get(:body) - |> Jason.decode!() - |> JOSE.JWK.from() - - def init(_opts) do - {:ok, build_state()} - end - - defp build_state do - {"google", config} = - Application.get_env(:openid_connect, :providers) - |> List.keyfind("google", 0) - - %{ - config: config, - jwk: @google_jwk, - document: @google_document - } - end - - def handle_cast({:reconfigure, state}, _state) do - {:noreply, state} - end - - def handle_call({:discovery_document, _provider}, _from, state) do - {:reply, Map.get(state, :document), state} - end - - def handle_call({:jwk, "google"}, _from, state) do - {:reply, Map.get(state, :jwk), state} - end - - def handle_call({:config, "google"}, _from, state) do - {:reply, Map.get(state, :config), state} - end - - def handle_call({:put, key, value}, _from, state) do - {:reply, :ok, Map.put(state, key, value)} - end - - def handle_call(_anything, _from, _state) do - {:reply, nil, %{}} - end -end diff --git a/test/test_helper.exs b/test/test_helper.exs index 213b010..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1 @@ -Mox.defmock(OpenIDConnect.HTTPClientMock, for: HTTPoison.Base) - ExUnit.start() From fa438f9260a22aaebf8b187b91d702761ba13527 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Wed, 11 Jan 2023 11:54:31 -0600 Subject: [PATCH 15/32] Disable some Credo checks and fix other issues --- .credo.exs | 2 -- lib/openid_connect/document/cache.ex | 7 +++---- test/openid_connect/document/cache_test.exs | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.credo.exs b/.credo.exs index a7e9373..32ade00 100644 --- a/.credo.exs +++ b/.credo.exs @@ -98,7 +98,6 @@ {Credo.Check.Readability.LargeNumbers, []}, {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, {Credo.Check.Readability.ModuleAttributeNames, []}, - {Credo.Check.Readability.ModuleDoc, []}, {Credo.Check.Readability.ModuleNames, []}, {Credo.Check.Readability.ParenthesesInCondition, []}, {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, @@ -132,7 +131,6 @@ {Credo.Check.Refactor.WithClauses, []}, {Credo.Check.Refactor.FilterFilter, []}, {Credo.Check.Refactor.RejectReject, []}, - {Credo.Check.Refactor.RedundantWithClauseResult, []}, # ## Warnings diff --git a/lib/openid_connect/document/cache.ex b/lib/openid_connect/document/cache.ex index cf7869b..1940505 100644 --- a/lib/openid_connect/document/cache.ex +++ b/lib/openid_connect/document/cache.ex @@ -15,7 +15,6 @@ defmodule OpenIDConnect.Document.Cache do end def put(pid \\ __MODULE__, uri, document) do - # TODO: we need to update timer in case the new document is expiring before it will exceed GenServer.cast(pid, {:put, uri, document}) end @@ -28,13 +27,13 @@ defmodule OpenIDConnect.Document.Cache do end def handle_cast({:put, uri, document}, state) do - if not document_expired?(document) do + if document_expired?(document) do + {:noreply, state} + else expires_in_seconds = expires_in_seconds(document.expires_at) timer_ref = Process.send_after(self(), :remove, :timer.seconds(expires_in_seconds)) state = Map.put(state, uri, {timer_ref, DateTime.utc_now(), document}) {:noreply, state} - else - {:noreply, state} end end diff --git a/test/openid_connect/document/cache_test.exs b/test/openid_connect/document/cache_test.exs index 734ecaf..06d25d8 100644 --- a/test/openid_connect/document/cache_test.exs +++ b/test/openid_connect/document/cache_test.exs @@ -99,7 +99,7 @@ defmodule OpenIDConnect.Document.CacheTest do describe ":gc" do test "doesn't do anything when cache is empty" do {:ok, pid} = start_link(name: :gc_test) - assert Enum.count(flush(pid)) == 0 + assert Enum.empty?(flush(pid)) send(pid, :gc) assert flush(pid) == %{} end From 1623decd71f8c481ca58e68ae1353bb0d9d4cf50 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Wed, 11 Jan 2023 12:03:31 -0600 Subject: [PATCH 16/32] Increase code coverage --- lib/openid_connect.ex | 5 +- test/fixtures/jwks/jwks.exs | 26 +++++++ test/openid_connect/document/cache_test.exs | 4 +- test/openid_connect/document_test.exs | 4 + test/openid_connect_test.exs | 83 ++++++++++++++++++++- 5 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/jwks/jwks.exs diff --git a/lib/openid_connect.ex b/lib/openid_connect.ex index d959624..a2a2918 100644 --- a/lib/openid_connect.ex +++ b/lib/openid_connect.ex @@ -96,7 +96,7 @@ defmodule OpenIDConnect do end end - defp fetch_scope(%{scope: scope}) when is_nil(scope) or scope == [], + defp fetch_scope(%{scope: scope}) when is_nil(scope) or scope == [] or scope == "", do: {:error, :invalid_scope} defp fetch_scope(%{scope: scope}) when is_binary(scope), @@ -123,6 +123,7 @@ defmodule OpenIDConnect do defp parse_response_type(nil), do: {:error, :invalid_response_type} defp parse_response_type([]), do: {:error, :invalid_response_type} + defp parse_response_type(""), do: {:error, :invalid_response_type} defp parse_response_type(response_type) when is_binary(response_type), do: {:ok, String.split(response_type)} @@ -259,8 +260,6 @@ defmodule OpenIDConnect do defp do_verify(%JOSE.JWK{} = jwk, token_alg, jwt), do: JOSE.JWS.verify_strict(jwk, [token_alg], jwt) - defp build_uri(nil, _params), do: nil - defp build_uri(uri, params) do query = URI.encode_query(params) diff --git a/test/fixtures/jwks/jwks.exs b/test/fixtures/jwks/jwks.exs new file mode 100644 index 0000000..132969a --- /dev/null +++ b/test/fixtures/jwks/jwks.exs @@ -0,0 +1,26 @@ +%{ + "keys" => [ + %{ + "alg" => "RS256", + "d" => + "X8TM24Zqbiha9geYYk_vZpANu16IadJLJLJ7ucTc3JaMbK8NCYNcHMoXKnNYPFxmq-UWAEIwh-2txOiOxuChVrblpfyE4SBJio1T0AUcCwmm8U6G-CsSHMMzWTt2dMTnArHjdyAIgOVRW5SVzhTTtaf4JY-47S-fbcJ7g0hmBbVih5i1sE2fad4I4qFHT-YFU_pnUHbteR6GQuRW4r03Eon8Aje6al2AxcYnfF8_cSOIOpkDgGavTtGYhhZPi2jZ7kPm6QGkNW5CyfEq5PGB6JOihw-XIFiiMzYgx052rnzoqALoLheXrI0By4kgHSmcqOOmq7aiOff45rlSbpsR", + "e" => "AQAB", + "kid" => "example@dockyard.com", + "kty" => "RSA", + "n" => + "qlKll8no4lPYXNSuTTnacpFHiXwPOv_htCYvIXmiR7CWhiiOHQqj7KWXIW7TGxyoLVIyeRM4mwvkLI-UgsSMYdEKTT0j7Ydjrr0zCunPu5Gxr2yOmcRaszAzGxJL5DwpA0V40RqMlm5OuwdqS4To_p9LlLxzMF6RZe1OqslV5RZ4Y8FmrWq6BV98eIziEHL0IKdsAIrrOYkkcLDdQeMNuTp_yNB8Xl2TdWSdsbRomrs2dCtCqZcXTsy2EXDceHvYhgAB33nh_w17WLrZQwMM-7kJk36Kk54jZd7i80AJf_s_plXn1mEh-L5IAL1vg3a9EOMFUl-lPiGqc3td_ykH", + "use" => "sig" + }, + %{ + "alg" => "RS256", + "d" => + "ezKeapIKr6YkDoWKarXmj3dytjlS1_kuSHCQs9HLFftqR0MYIJICcBeXz2mmYUjkl0cpnce_xDCS2IOfV6KUpRhkNsOWHNtjqKXs-YHMapffgo_zeoZrdIdiDEqvm1X4D4qg8qbN2IGhhFsb8WO-aP3otSsSXreA7ibaDusxBNbxxWkaEwikBy61A1MTu5CR5GLYgsgQsZnawxJqDo1-QH6b-5E05XHD2PHG_xADLePC9zDsCr0kpCw1c-DuJ0SdKzV-OxoCbHuTRRP8Df7WztsLyFxYtDdZhgYOGmofPa_w8L6jLZTecixhBuSzV3OSGxua7eGd7MQDHf1vcpbh", + "e" => "AQAB", + "kid" => "example@dockyard.com", + "kty" => "RSA", + "n" => + "iUteZhwFt_wvc8QR5rfh7zShXqwlRMMwB-9kst-A2ixeUXrBkwqQrEoAy_FUOFgQTIb8gRFvzlc7oB6cWC-OFdt5XLQsBV6fTtnkEVZtVdze22V42qz3y0l5lztKGXJYjLSbB6kUF1SiT7wpbT7J-M9bSkxwdVWQYAMZsPg71IZ7qX4JyxcvUnqnXEseHMunsWcGGdqR6OcVQylAlKBi_biKSjbXVavWjXbk25-IDs6YQxNGu7RF-DZoDNYSyEFZWN9pY_wdPZy2RvgW_5okNjffcecg-HPMHRFHsKbIsHlX1XLJ7gbY79MtCTFQym4ZcrJqn42r99dw6242KoAB", + "use" => "sig" + } + ] +} diff --git a/test/openid_connect/document/cache_test.exs b/test/openid_connect/document/cache_test.exs index 06d25d8..a4b3027 100644 --- a/test/openid_connect/document/cache_test.exs +++ b/test/openid_connect/document/cache_test.exs @@ -98,14 +98,14 @@ defmodule OpenIDConnect.Document.CacheTest do describe ":gc" do test "doesn't do anything when cache is empty" do - {:ok, pid} = start_link(name: :gc_test) + {:ok, pid} = start_link(name: :gc_test1) assert Enum.empty?(flush(pid)) send(pid, :gc) assert flush(pid) == %{} end test "removes excessive entries from cache" do - {:ok, pid} = start_link(name: :gc_test) + {:ok, pid} = start_link(name: :gc_test2) documents = for i <- 1..2000 do diff --git a/test/openid_connect/document_test.exs b/test/openid_connect/document_test.exs index cb9bb34..f79a0dc 100644 --- a/test/openid_connect/document_test.exs +++ b/test/openid_connect/document_test.exs @@ -4,6 +4,10 @@ defmodule OpenIDConnect.DocumentTest do import OpenIDConnect.Document describe "fetch_document/1" do + test "returns error when URL is nil" do + assert fetch_document(nil) == {:error, :invalid_discovery_document_uri} + end + test "returns valid document from a given url" do {_bypass, uri} = start_fixture("auth0") diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index c95bd0c..89f82d4 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -13,7 +13,7 @@ defmodule OpenIDConnectTest do } describe "authorization_uri/2" do - test "generates authorization url" do + test "generates authorization url with scope and response_type as binaries" do {_bypass, uri} = start_fixture("google") config = %{@config | discovery_document_uri: uri} @@ -26,6 +26,63 @@ defmodule OpenIDConnectTest do "&scope=openid+email+profile"} end + test "generates authorization url with scope as enum" do + {_bypass, uri} = start_fixture("google") + config = %{@config | discovery_document_uri: uri, scope: ["openid", "email", "profile"]} + + assert authorization_uri(config) == + {:ok, + "https://accounts.google.com/o/oauth2/v2/auth?" <> + "client_id=CLIENT_ID" <> + "&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <> + "&response_type=code+id_token+token" <> + "&scope=openid+email+profile"} + end + + test "generates authorization url with response_type as enum" do + {_bypass, uri} = start_fixture("google") + + config = %{ + @config + | discovery_document_uri: uri, + response_type: ["code", "id_token", "token"] + } + + assert authorization_uri(config) == + {:ok, + "https://accounts.google.com/o/oauth2/v2/auth?" <> + "client_id=CLIENT_ID" <> + "&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <> + "&response_type=code+id_token+token" <> + "&scope=openid+email+profile"} + end + + test "returns error on empty scope" do + {_bypass, uri} = start_fixture("google") + + config = %{@config | discovery_document_uri: uri, scope: nil} + assert authorization_uri(config) == {:error, :invalid_scope} + + config = %{@config | discovery_document_uri: uri, scope: ""} + assert authorization_uri(config) == {:error, :invalid_scope} + + config = %{@config | discovery_document_uri: uri, scope: []} + assert authorization_uri(config) == {:error, :invalid_scope} + end + + test "returns error on empty response_type" do + {_bypass, uri} = start_fixture("google") + + config = %{@config | discovery_document_uri: uri, response_type: nil} + assert authorization_uri(config) == {:error, :invalid_response_type} + + config = %{@config | discovery_document_uri: uri, response_type: ""} + assert authorization_uri(config) == {:error, :invalid_response_type} + + config = %{@config | discovery_document_uri: uri, response_type: []} + assert authorization_uri(config) == {:error, :invalid_response_type} + end + test "adds optional params" do {_bypass, uri} = start_fixture("google") config = %{@config | discovery_document_uri: uri} @@ -290,6 +347,30 @@ defmodule OpenIDConnectTest do assert verify(config, token) == {:ok, claims} end + test "returns claims when encoded token is valid using multiple keys" do + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwks.exs") + + jwk = + jwks + |> Map.fetch!("keys") + |> List.first() + |> JOSE.JWK.from() + + {_, jwk_pubkey} = JOSE.JWK.to_public_map(jwk) + + {_bypass, uri} = start_fixture("vault", %{"jwks" => jwk_pubkey}) + config = %{@config | discovery_document_uri: uri} + + claims = %{"email" => "brian@example.com"} + + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() + + assert verify(config, token) == {:ok, claims} + end + test "returns error when token is altered" do {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") jwk = JOSE.JWK.from(jwks) From ff4552e45f43241cf3c6e69b2b113869ae6fdfda Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 13 Jan 2023 10:55:30 -0600 Subject: [PATCH 17/32] Fix gc message tuple --- lib/openid_connect/document/cache.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openid_connect/document/cache.ex b/lib/openid_connect/document/cache.ex index 1940505..fa48b9d 100644 --- a/lib/openid_connect/document/cache.ex +++ b/lib/openid_connect/document/cache.ex @@ -31,7 +31,7 @@ defmodule OpenIDConnect.Document.Cache do {:noreply, state} else expires_in_seconds = expires_in_seconds(document.expires_at) - timer_ref = Process.send_after(self(), :remove, :timer.seconds(expires_in_seconds)) + timer_ref = Process.send_after(self(), {:remove, uri}, :timer.seconds(expires_in_seconds)) state = Map.put(state, uri, {timer_ref, DateTime.utc_now(), document}) {:noreply, state} end From a601494fc6c6b4cba4818faa24d780b4b0ef10e8 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Tue, 24 Jan 2023 12:07:35 -0600 Subject: [PATCH 18/32] Allow to override transport options for http client --- lib/openid_connect/application.ex | 13 ++++++++++-- test/openid_connect/application_test.exs | 26 ++++++++++++++++++++++++ test/openid_connect_test.exs | 2 +- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 test/openid_connect/application_test.exs diff --git a/lib/openid_connect/application.ex b/lib/openid_connect/application.ex index ba6f5d9..f190ab1 100644 --- a/lib/openid_connect/application.ex +++ b/lib/openid_connect/application.ex @@ -6,10 +6,19 @@ defmodule OpenIDConnect.Application do Supervisor.start_link(children(), opts) end - defp children do + def children do [ - {Finch, name: OpenIDConnect.Finch}, + {Finch, + name: OpenIDConnect.Finch, + pools: %{ + default: pool_opts() + }}, OpenIDConnect.Document.Cache ] end + + defp pool_opts do + transport_opts = Application.get_env(:openid_connect, :finch_transport_opts, []) + [conn_opts: [transport_opts: transport_opts]] + end end diff --git a/test/openid_connect/application_test.exs b/test/openid_connect/application_test.exs new file mode 100644 index 0000000..b771b68 --- /dev/null +++ b/test/openid_connect/application_test.exs @@ -0,0 +1,26 @@ +defmodule OpenIDConnect.ApplicationTest do + use ExUnit.Case, async: true + import OpenIDConnect.Application + + test "allows to override Finch transport options" do + assert children() == [ + {Finch, + [name: OpenIDConnect.Finch, pools: %{default: [conn_opts: [transport_opts: []]]}]}, + OpenIDConnect.Document.Cache + ] + + transport_opts = [cacertfile: "foo.pem"] + Application.put_env(:openid_connect, :finch_transport_opts, transport_opts) + + assert children() == [ + {Finch, + [ + name: OpenIDConnect.Finch, + pools: %{default: [conn_opts: [transport_opts: transport_opts]]} + ]}, + OpenIDConnect.Document.Cache + ] + + Application.put_env(:openid_connect, :finch_transport_opts, []) + end +end diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index 89f82d4..4c5aff0 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -1,5 +1,5 @@ defmodule OpenIDConnectTest do - use ExUnit.Case + use ExUnit.Case, async: true import OpenIDConnect.Fixtures import OpenIDConnect From 1a1dd8294584de7c983df8aad322deea90bcb3b8 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 27 Jan 2023 10:21:55 -0600 Subject: [PATCH 19/32] Allow claims_supported to be nil --- lib/openid_connect/document.ex | 7 ++- .../http/cognito/discovery_document.exs | 49 +++++++++++++++++++ test/fixtures/http/cognito/jwks.exs | 26 ++++++++++ test/openid_connect/document_test.exs | 38 +++++++++++++- 4 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/http/cognito/discovery_document.exs create mode 100644 test/fixtures/http/cognito/jwks.exs diff --git a/lib/openid_connect/document.ex b/lib/openid_connect/document.ex index 45a1eb5..1d42b26 100644 --- a/lib/openid_connect/document.ex +++ b/lib/openid_connect/document.ex @@ -126,7 +126,7 @@ defmodule OpenIDConnect.Document do end), claims_supported: Map.get(document_json, "claims_supported") - |> Enum.sort() + |> sort_claims() } {:ok, document} @@ -135,9 +135,12 @@ defmodule OpenIDConnect.Document do end end + defp sort_claims(nil), do: nil + defp sort_claims(claims), do: Enum.sort(claims) + defp from_certs(certs) do {:ok, JOSE.JWK.from(certs)} rescue - _ -> {:error, :invalid_jwks_certificated} + _ -> {:error, :invalid_jwks_certificates} end end diff --git a/test/fixtures/http/cognito/discovery_document.exs b/test/fixtures/http/cognito/discovery_document.exs new file mode 100644 index 0000000..55f0615 --- /dev/null +++ b/test/fixtures/http/cognito/discovery_document.exs @@ -0,0 +1,49 @@ +%{ + status_code: 200, + body: %{ + "authorization_endpoint" => "https://DOMAIN/oauth2/authorize", + "id_token_signing_alg_values_supported" => [ + "RS256" + ], + "issuer" => "https://cognito-idp.REGION.amazonaws.com/REGION-CODE", + "jwks_uri" => "https://cognito-idp.REGIONamazonaws.com/REGION-CODE/.well-known/jwks.json", + "response_types_supported" => [ + "code", + "token" + ], + "scopes_supported" => [ + "openid", + "email", + "phone", + "profile" + ], + "subject_types_supported" => [ + "public" + ], + "token_endpoint" => "https://DOMAIN/oauth2/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "client_secret_post" + ], + "userinfo_endpoint" => "https://DOMAIN/oauth2/userInfo" + }, + headers: [ + {"Cache-Control", "max-age=86400, private"}, + {"Content-Type", "application/json; charset=utf-8"}, + {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, + {"X-Content-Type-Options", "nosniff"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "GET, OPTIONS"}, + {"P3P", "CP=\"DSP CUR OTPi IND OTRi ONL FIN\""}, + {"x-ms-request-id", "d81e7f56-0451-4de4-a5c5-4af112d02001"}, + {"x-ms-ests-server", "2.1.14006.10 - NCUS ProdSlices"}, + {"X-XSS-Protection", "0"}, + {"Set-Cookie", + "fpc=AuKLSwY1b3xLiInKP16p3E4; expires=Mon, 12-Dec-2022 19:36:30 GMT; path=/; secure; HttpOnly; SameSite=None"}, + {"Set-Cookie", + "esctx=AQABAAAAAAD--DLA3VO7QrddgJg7Wevr2ALKzZMjPY-Tt7ffB-f_7y4AMTUR-4m-AQDAi0jJ1K4_N7dY0CZmKZdSweQPMgerZ-TeKnty43nfmYRZS2G39bKUZp5erQLwiB9rkuLis4_ee_cAZK7nh1pkqOh0_t52P9svf75Le0-ex8iyPVhexTbIROTaaYvo6Fl9DFqOtZOnmQplc6ken-ddUcLbnZRSKOTFdr03VB8oSt5gD2BBw2e5qeBuocgX0hS-W-FNbG0gAA; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None"}, + {"Set-Cookie", "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly"}, + {"Set-Cookie", "stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"}, + {"Date", "Sat, 12 Nov 2022 19:36:29 GMT"} + ] +} diff --git a/test/fixtures/http/cognito/jwks.exs b/test/fixtures/http/cognito/jwks.exs new file mode 100644 index 0000000..a5972e5 --- /dev/null +++ b/test/fixtures/http/cognito/jwks.exs @@ -0,0 +1,26 @@ +%{ + status_code: 200, + body: %{ + "keys" => [ + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "f451345fad08101bfb345cf642a2da9267b9ebeb", + "kty" => "RSA", + "n" => + "ppFPAZUqIVqCf_SffT6xDCXu1R7aRoT6TNT5_Q8PKxkkqbOVysJPNwliF-486VeM8KNW8onFOv0GkP0lJ2ASrVgyMG1qmlGUlKug64dMQXPxSlVUCXCPN676W5IZTvT0tD2byM_29HZXnOifRg-d7PRRvIBLSUWe-fGb1-tP2w65SOW-W6LuOjGzLNPJFYQvHyUx_uXHOCfIoSb8kaMwx8bCWvKc76yT0DG1wcygGXKuFQHW-Sdi1j_6bF19lVu30DX-jhYsNMUnGUr6g2iycQ50pWMORZqvcHVOH1bbDrWuz0b564sK0ET2B3XDR37djNQ305PxiQZaBStm-hM8Aw", + "use" => "sig" + }, + %{ + "alg" => "RS256", + "e" => "AQAB", + "kid" => "713fd68c966e29380981edc0164a2f6c06c5702a", + "kty" => "RSA", + "n" => + "z8PS6saDU3h5ZbQb3Lwl_Arwgu65ECMi79KUlzx4tqk8bgxtaaHcqyvWqVdsA9H6Q2ZtQhBZivqV4Jg0HoPHcEwv46SEziFQNR2LH86e-WIDI5pk2NKg_9cFMee9Mz7f_NSQJ3uyD1pu86bdUTYhCw57DbEVDOuubClNMUV456dWx7dx5W4kdcQe63vGg9LXQ-9PPz9AL-0ZKr8eQEHp4KRfRUfngjqjYBMTFuuo38l94KR99B04Z-FboGnqYLgNxctwZ9eXbCerb9bV5-Q9Gb3zoo0x1h90tFdgmC2ZU1xcIIjHmFqJ29mSDZHYAAYtMNAeWreK4gqWJunc9o0vpQ", + "use" => "sig" + } + ] + }, + headers: [] +} diff --git a/test/openid_connect/document_test.exs b/test/openid_connect/document_test.exs index f79a0dc..a96aabe 100644 --- a/test/openid_connect/document_test.exs +++ b/test/openid_connect/document_test.exs @@ -53,7 +53,16 @@ defmodule OpenIDConnect.DocumentTest do end test "supports all gateway providers" do - for provider <- ["auth0", "azure", "google", "keycloak", "okta", "onelogin", "vault"] do + for provider <- [ + "auth0", + "azure", + "google", + "keycloak", + "okta", + "onelogin", + "vault", + "cognito" + ] do {_bypass, uri} = start_fixture(provider) assert {:ok, document} = fetch_document(uri) assert not is_nil(document.jwks) @@ -67,6 +76,33 @@ defmodule OpenIDConnect.DocumentTest do assert {:ok, ^document} = fetch_document(uri) end + test "returns error when JSWKS is invalid" do + invalid_jwks = %{ + "keys" => [ + %{ + "kid" => "1234example=", + "alg" => "RS256", + "kty" => "RSA", + "e" => "AQAB", + "n" => "1234567890", + "use" => "sig" + }, + %{ + "kid" => "5678example=", + "alg" => "RS256", + "kty" => "RSA", + "e" => "AQAB", + "n" => "987654321", + "use" => "sig" + } + ] + } + + {_bypass, uri} = start_fixture("auth0", %{"jwks" => invalid_jwks}) + + assert fetch_document(uri) == {:error, :invalid_jwks_certificates} + end + test "handles non 2XX response codes" do bypass = Bypass.open() From f00c996f3463613afb8a6a25cf4214bbf099b5d1 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Thu, 20 Apr 2023 15:30:58 -0700 Subject: [PATCH 20/32] Add userinfo endpoint support --- lib/openid_connect.ex | 17 ++++++ lib/openid_connect/document.ex | 2 + test/fixtures/http/google/userinfo.exs | 31 ++++++++++ test/openid_connect_test.exs | 81 ++++++++++++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 test/fixtures/http/google/userinfo.exs diff --git a/lib/openid_connect.ex b/lib/openid_connect.ex index a2a2918..2fc310b 100644 --- a/lib/openid_connect.ex +++ b/lib/openid_connect.ex @@ -260,6 +260,23 @@ defmodule OpenIDConnect do defp do_verify(%JOSE.JWK{} = jwk, token_alg, jwt), do: JOSE.JWS.verify_strict(jwk, [token_alg], jwt) + def fetch_userinfo(config, access_token) do + discovery_document_uri = config.discovery_document_uri + + headers = [{"Authorization", "Bearer #{access_token}"}] + + with {:ok, document} <- Document.fetch_document(discovery_document_uri), + request = Finch.build(:get, document.userinfo_endpoint, headers), + {:ok, %Finch.Response{body: response, status: status}} when status in 200..299 <- + Finch.request(request, OpenIDConnect.Finch), + {:ok, json} <- Jason.decode(response) do + {:ok, json} + else + {:ok, %Finch.Response{body: response, status: status}} -> {:error, {status, response}} + other -> other + end + end + defp build_uri(uri, params) do query = URI.encode_query(params) diff --git a/lib/openid_connect/document.ex b/lib/openid_connect/document.ex index 1d42b26..bab3a25 100644 --- a/lib/openid_connect/document.ex +++ b/lib/openid_connect/document.ex @@ -8,6 +8,7 @@ defmodule OpenIDConnect.Document do authorization_endpoint: nil, end_session_endpoint: nil, token_endpoint: nil, + userinfo_endpoint: nil, claims_supported: nil, response_types_supported: nil, jwks: nil, @@ -116,6 +117,7 @@ defmodule OpenIDConnect.Document do authorization_endpoint: Map.fetch!(document_json, "authorization_endpoint"), end_session_endpoint: Map.get(document_json, "end_session_endpoint"), token_endpoint: Map.fetch!(document_json, "token_endpoint"), + userinfo_endpoint: Map.fetch!(document_json, "userinfo_endpoint"), response_types_supported: Map.get(document_json, "response_types_supported") |> Enum.map(fn response_type -> diff --git a/test/fixtures/http/google/userinfo.exs b/test/fixtures/http/google/userinfo.exs new file mode 100644 index 0000000..ded1a96 --- /dev/null +++ b/test/fixtures/http/google/userinfo.exs @@ -0,0 +1,31 @@ +%{ + body: %{ + "sub" => "353690423699814251281", + "name" => "Ada Lovelace", + "given_name" => "Ada", + "family_name" => "Lovelace", + "picture" => + "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg", + "email" => "ada@example.com", + "email_verified" => true, + "locale" => "en" + }, + headers: [ + {"Date", "Thu, 17 Dec 2020 14:29:16 GMT"}, + {"Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"}, + {"Expires", "Mon, 01 Jan 1990 00:00:00 GMT"}, + {"Pragma", "no-cache"}, + {"Content-Type", "application/json; charset=utf-8"}, + {"Vary", "X-Origin"}, + {"Vary", "Referer"}, + {"Server", "ESF"}, + {"X-XSS-Protection", "0"}, + {"X-Frame-Options", "SAMEORIGIN"}, + {"X-Content-Type-Options", "nosniff"}, + {"Alt-Svc", + "h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\""}, + {"Accept-Ranges", "none"}, + {"Vary", "Origin,Accept-Encoding"} + ], + status_code: 200 +} diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index 4c5aff0..18b631f 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -410,4 +410,85 @@ defmodule OpenIDConnectTest do {:error, %Mint.TransportError{reason: :econnrefused}} end end + + describe "fetch_userinfo/2" do + test "returns user info using endpoint from discovery document" do + bypass = Bypass.open() + test_pid = self() + + { + userinfo_status_code, + userinfo_response_attrs, + userinfo_response_headers + } = load_fixture("google", "userinfo") + + Bypass.expect_once(bypass, "GET", "/userinfo", fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + + conn = + Enum.reduce(userinfo_response_headers, conn, fn {k, v}, conn -> + Plug.Conn.put_resp_header(conn, k, v) + end) + + send(test_pid, {:req, body, conn.req_headers}) + Plug.Conn.resp(conn, userinfo_status_code, Jason.encode!(userinfo_response_attrs)) + end) + + userinfo_endpoint = "http://localhost:#{bypass.port}/userinfo" + + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") + jwk = JOSE.JWK.from(jwks) + {_, jwk_pubkey} = JOSE.JWK.to_public_map(jwk) + + {_bypass, uri} = + start_fixture("vault", %{ + "jwks" => jwk_pubkey, + "userinfo_endpoint" => userinfo_endpoint + }) + + config = %{@config | discovery_document_uri: uri} + + claims = %{"email" => userinfo_response_attrs["email"]} + + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() + + assert {:ok, userinfo} = fetch_userinfo(config, token) + + assert userinfo == userinfo_response_attrs + + assert_receive {:req, "", headers} + assert {"authorization", "Bearer #{token}"} in headers + end + + test "returns error when userinfo endpoint is not available" do + bypass = Bypass.open() + userinfo_endpoint = "http://localhost:#{bypass.port}/userinfo" + Bypass.down(bypass) + + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") + jwk = JOSE.JWK.from(jwks) + {_, jwk_pubkey} = JOSE.JWK.to_public_map(jwk) + + {_bypass, uri} = + start_fixture("vault", %{ + "jwks" => jwk_pubkey, + "userinfo_endpoint" => userinfo_endpoint + }) + + config = %{@config | discovery_document_uri: uri} + + claims = %{"email" => "foo@john.com"} + + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() + + assert fetch_userinfo(config, token) == + {:error, %Mint.TransportError{reason: :econnrefused}} + end + end end From 03265d05551d82fd29939de013ee7fccbb8e966b Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Thu, 20 Apr 2023 15:48:57 -0700 Subject: [PATCH 21/32] Verify token expiration and aud value --- lib/openid_connect.ex | 60 ++++++++++++++++-- test/openid_connect_test.exs | 114 ++++++++++++++++++++++++++++++++++- 2 files changed, 166 insertions(+), 8 deletions(-) diff --git a/lib/openid_connect.ex b/lib/openid_connect.ex index 2fc310b..3b0185c 100644 --- a/lib/openid_connect.ex +++ b/lib/openid_connect.ex @@ -52,7 +52,8 @@ defmodule OpenIDConnect do required(:client_secret) => client_secret(), required(:redirect_uri) => redirect_uri(), required(:response_type) => response_type(), - required(:scope) => scope() + required(:scope) => scope(), + optional(:leeway) => non_neg_integer() } @typedoc """ @@ -216,8 +217,10 @@ defmodule OpenIDConnect do {:ok, decoded_protected} <- Jason.decode(protected), {:ok, token_alg} <- Map.fetch(decoded_protected, "alg"), {:ok, document} <- Document.fetch_document(discovery_document_uri), - {true, claims, _jwk} <- do_verify(document.jwks, token_alg, jwt) do - Jason.decode(claims) + {true, claims, _jwk} <- verify_signature(document.jwks, token_alg, jwt), + {:ok, unverified_claims} <- Jason.decode(claims), + {:ok, verified_claims} <- verify_claims(unverified_claims, config) do + {:ok, verified_claims} else {:error, %Jason.DecodeError{}} -> {:error, {:invalid_jwt, "token claims did not contain a JSON payload"}} @@ -225,6 +228,9 @@ defmodule OpenIDConnect do {:error, :peek_protected} -> {:error, {:invalid_jwt, "invalid token format"}} + {:error, invalid_claim, message} -> + {:error, {:invalid_jwt, "invalid #{invalid_claim} claim: #{message}"}} + :error -> {:error, {:invalid_jwt, "no `alg` found in token"}} @@ -245,11 +251,11 @@ defmodule OpenIDConnect do _ -> {:error, :peek_protected} end - defp do_verify(%JOSE.JWK{keys: {:jose_jwk_set, jwks}}, token_alg, jwt) do + defp verify_signature(%JOSE.JWK{keys: {:jose_jwk_set, jwks}}, token_alg, jwt) do Enum.find_value(jwks, {false, "{}", jwt}, fn jwk -> jwk |> JOSE.JWK.from() - |> do_verify(token_alg, jwt) + |> verify_signature(token_alg, jwt) |> case do {false, _claims, _jwt} -> false verified_claims -> verified_claims @@ -257,9 +263,51 @@ defmodule OpenIDConnect do end) end - defp do_verify(%JOSE.JWK{} = jwk, token_alg, jwt), + defp verify_signature(%JOSE.JWK{} = jwk, token_alg, jwt), do: JOSE.JWS.verify_strict(jwk, [token_alg], jwt) + defp verify_claims(claims, config) do + leeway = Map.get(config, :leeway, 30) + client_id = Map.fetch!(config, :client_id) + + with :ok <- verify_exp_claim(claims, leeway), + :ok <- verify_aud_claim(claims, client_id) do + {:ok, claims} + end + end + + defp verify_exp_claim(claims, leeway) do + case Map.fetch(claims, "exp") do + {:ok, exp} when is_integer(exp) -> + epoch = DateTime.utc_now() |> DateTime.to_unix() + + if epoch < exp + leeway, + do: :ok, + else: {:error, "exp", "token has expired"} + + {:ok, _exp} -> + {:error, "exp", "is invalid"} + + :error -> + {:error, "exp", "missing"} + end + end + + defp verify_aud_claim(claims, expected_aud) do + case Map.fetch(claims, "aud") do + {:ok, aud} -> + if audience_matches?(aud, expected_aud), + do: :ok, + else: {:error, "aud", "token is intended for another application"} + + :error -> + {:error, "aud", "missing"} + end + end + + defp audience_matches?(aud, expected_aud) when is_list(aud), do: Enum.member?(aud, expected_aud) + defp audience_matches?(aud, expected_aud), do: aud === expected_aud + def fetch_userinfo(config, access_token) do discovery_document_uri = config.discovery_document_uri diff --git a/test/openid_connect_test.exs b/test/openid_connect_test.exs index 18b631f..277d48c 100644 --- a/test/openid_connect_test.exs +++ b/test/openid_connect_test.exs @@ -337,7 +337,11 @@ defmodule OpenIDConnectTest do {_bypass, uri} = start_fixture("vault", %{"jwks" => jwk_pubkey}) config = %{@config | discovery_document_uri: uri} - claims = %{"email" => "brian@example.com"} + claims = %{ + "email" => "brian@example.com", + "exp" => DateTime.utc_now() |> DateTime.add(10, :second) |> DateTime.to_unix(), + "aud" => config.client_id + } {_alg, token} = jwk @@ -361,7 +365,24 @@ defmodule OpenIDConnectTest do {_bypass, uri} = start_fixture("vault", %{"jwks" => jwk_pubkey}) config = %{@config | discovery_document_uri: uri} - claims = %{"email" => "brian@example.com"} + claims = %{ + "email" => "brian@example.com", + "exp" => DateTime.utc_now() |> DateTime.add(10, :second) |> DateTime.to_unix(), + "aud" => config.client_id + } + + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() + + assert verify(config, token) == {:ok, claims} + + claims = %{ + "email" => "brian@example.com", + "exp" => DateTime.utc_now() |> DateTime.add(-29, :second) |> DateTime.to_unix(), + "aud" => config.client_id + } {_alg, token} = jwk @@ -371,6 +392,95 @@ defmodule OpenIDConnectTest do assert verify(config, token) == {:ok, claims} end + test "returns error when token is expired" do + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") + jwk = JOSE.JWK.from(jwks) + {_, jwk_pubkey} = JOSE.JWK.to_public_map(jwk) + + {_bypass, uri} = start_fixture("vault", %{"jwks" => jwk_pubkey}) + config = %{@config | discovery_document_uri: uri} + + claims = %{ + "email" => "brian@example.com", + "exp" => DateTime.utc_now() |> DateTime.add(-31, :second) |> DateTime.to_unix(), + "aud" => config.client_id + } + + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() + + assert verify(config, token) == + {:error, {:invalid_jwt, "invalid exp claim: token has expired"}} + end + + test "returns error when token expiration is not set" do + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") + jwk = JOSE.JWK.from(jwks) + {_, jwk_pubkey} = JOSE.JWK.to_public_map(jwk) + + {_bypass, uri} = start_fixture("vault", %{"jwks" => jwk_pubkey}) + config = %{@config | discovery_document_uri: uri} + + claims = %{ + "email" => "brian@example.com", + "aud" => config.client_id + } + + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() + + assert verify(config, token) == {:error, {:invalid_jwt, "invalid exp claim: missing"}} + end + + test "returns error when aud claim is for another application" do + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") + jwk = JOSE.JWK.from(jwks) + {_, jwk_pubkey} = JOSE.JWK.to_public_map(jwk) + + {_bypass, uri} = start_fixture("vault", %{"jwks" => jwk_pubkey}) + config = %{@config | discovery_document_uri: uri} + + claims = %{ + "email" => "brian@example.com", + "exp" => DateTime.utc_now() |> DateTime.to_unix(), + "aud" => "foo" + } + + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() + + assert verify(config, token) == + {:error, + {:invalid_jwt, "invalid aud claim: token is intended for another application"}} + end + + test "returns error when aud claim is not set" do + {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") + jwk = JOSE.JWK.from(jwks) + {_, jwk_pubkey} = JOSE.JWK.to_public_map(jwk) + + {_bypass, uri} = start_fixture("vault", %{"jwks" => jwk_pubkey}) + config = %{@config | discovery_document_uri: uri} + + claims = %{ + "email" => "brian@example.com", + "exp" => DateTime.utc_now() |> DateTime.to_unix() + } + + {_alg, token} = + jwk + |> JOSE.JWS.sign(Jason.encode!(claims), %{"alg" => "RS256"}) + |> JOSE.JWS.compact() + + assert verify(config, token) == {:error, {:invalid_jwt, "invalid aud claim: missing"}} + end + test "returns error when token is altered" do {jwks, []} = Code.eval_file("test/fixtures/jwks/jwk.exs") jwk = JOSE.JWK.from(jwks) From 08b49eb41705398a4d6eab19e1d8b25c9059ad5f Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 11:42:30 -0700 Subject: [PATCH 22/32] Reset and fixup README and LICENSE files --- LICENSE.md | 2 +- README.md | 93 ++++++++++++++++++++---------------------------------- 2 files changed, 35 insertions(+), 60 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 8c3e3ab..cceb743 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -4,4 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 815e5fb..502a8a3 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,42 @@ # OpenIDConnect -Firezone's fork of [DockYard's client library](https://github.com/dockyard/openid_connect) for consuming and working with OpenID Connect Providers. +Client library for consuming and working with OpenID Connect Providers -**[OpenIDConnect is originally built and maintained by DockYard, contact us for expert Elixir and Phoenix consulting](https://dockyard.com/phoenix-consulting)**. - -**This fork is maintained by the Firezone team**. +**[OpenIDConnect is built and maintained by DockYard, contact us for expert Elixir and Phoenix consulting](https://dockyard.com/phoenix-consulting)**. ## Installation -~~[Available in Hex](https://hex.pm/packages/openid_connect), the package can be installed as:~~ - -EDIT: This fork is *not* published to Hex (yet). +[Available in Hex](https://hex.pm/packages/openid_connect), the package can be installed as: Add `openid_connect` to your list of dependencies in `mix.exs`: ```elixir def deps do - [{:openid_connect, github: "firezone/openid_connect"}] + [{:openid_connect, "~> 1.0.0"}] end ``` -**Note**: This library is not published to Hex (yet). Install from GitHub instead. - ## Getting Started - ### Configuration -You should add the configuration settings for each of your providers into one of your app's configuration: +Most of the functions expect a `config` map which means application developer should take care for the storage of those options, eg: ```elixir -config :my_app, :openid_connect_providers, - google: [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", - client_id: "CLIENT_ID", - client_secret: "CLIENT_SECRET", - redirect_uri: "https://example.com/session", - response_type: "code", - scope: "openid email profile" - ] +google_config = [ + discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", + client_id: "CLIENT_ID", + client_secret: "CLIENT_SECRET", + redirect_uri: "https://example.com/session", + response_type: "code", + scope: "openid email profile" +] + +authorization_uri(google_config) ``` -You *must* setup with your provider first. Your provider will have the correct data to provider for the config settings above. - Most major OAuth2 providers have added support for OpenIDConnect. [See a short list of most major adopters of OpenIDConnect](https://en.wikipedia.org/wiki/List_of_OAuth_providers). -> You can add multiple providers in the list. The key for each provider is just a reference that you'll -> use with the `OpenIDConnect` module in your app code. - -### Worker - -Next add the `OpenIDConnect.Worker` to your app's supervisor along with your provider configs. - -```elixir -children = - [ - { - OpenIDConnect.Worker, - Application.get_env(:my_app, :openid_connect_providers) - }, - ] - -opts = [strategy: :one_for_one, name: MyApp.Supervisor] -Supervisor.start_link(children, opts) -``` - ### Usage In your app code you will need to do a few things: @@ -80,12 +52,12 @@ In your app code you will need to do a few things: You can build the correct authorization URI for a provider with: ```elixir -OpenIDConnect.authorization_uri(:google) +OpenIDConnect.authorization_uri(google_config) ``` In this case we are requesting that `OpenIDConnect` build the authorization URI for -the `:google` provider that we setup on in configuration above. You should use this URI for -your users to link out to for authenticating with the given provider +the google provider. You should use this URI for your users to link out to for +authenticating with the given provider #### Handling the redirect from the provider @@ -98,7 +70,7 @@ The JSON Web Token (JWT) must be fetched, using the key/value pairs from the `re part of the redirect to your application: ```elixir -{:ok, tokens} = OpenIDConnect.fetch_tokens(:google, %{code: params["code"]}) +{:ok, tokens} = OpenIDConnect.fetch_tokens(google_config, %{code: params["code"]}) ``` #### Verify the JWT @@ -106,7 +78,7 @@ part of the redirect to your application: The JWT is encrypted and it should always be verified with the JSON Web Keys (JWK) for the provider: ```elixir -{:ok, claims} = OpenIDConnect.verify(:google, tokens["id_token"]) +{:ok, claims} = OpenIDConnect.verify(google_config, tokens["id_token"]) ``` The `claims` is a payload with the information from the `scopes` you requested of the provider. @@ -126,14 +98,17 @@ get("/session/authorization-uri", SessionController, :authorization_uri) # session_controller.ex # you could also take the `provider` as a query param to pass into the function def authorization_uri(conn, _params) do - json(conn, %{uri: OpenIDConnect.authorization_uri(:google)}) + google_config = Application.fetch_env!(:my_app, :google_oidc_config) + json(conn, %{uri: OpenIDConnect.authorization_uri(google_config)}) end # The `Authentication` module here is an imaginary interface for setting session state def create(conn, params) do - with {:ok, tokens} <- OpenIDConnect.fetch_tokens(:google, params["code"]), - {:ok, claims} <- OpenIDConnect.verify(:google, tokens["id_token"]), - {:ok, user} <- Authentication.call(:google, Repo, claims) do + google_config = Application.fetch_env!(:my_app, :google_oidc_config) + + with {:ok, tokens} <- OpenIDConnect.fetch_tokens(google_config, params["code"]), + {:ok, claims} <- OpenIDConnect.verify(google_config, tokens["id_token"]), + {:ok, user} <- Authentication.call(google_config, Repo, claims) do conn |> put_status(200) @@ -144,30 +119,30 @@ def create(conn, params) do end ``` -## Authors ## +## Authors -* [Brian Cardarella](http://twitter.com/bcardarella) +- [Brian Cardarella](http://twitter.com/bcardarella) -[We are very thankful for the many contributors](https://github.com//openid_connect/graphs/contributors) +[We are very thankful for the many contributors](https://github.com/dockyard/openid_connect/graphs/contributors) -## Versioning ## +## Versioning This library follows [Semantic Versioning](http://semver.org) -## Looking for help with your Elixir project? ## +## Looking for help with your Elixir project? [At DockYard we are ready to help you build your next Elixir project](https://dockyard.com/phoenix-consulting). We have a unique expertise in Elixir and Phoenix development that is unmatched. [Get in touch!](https://dockyard.com/contact/hire-us) At DockYard we love Elixir! You can [read our Elixir blog posts](https://dockyard.com/blog/categories/elixir) or come visit us at [The Boston Elixir Meetup](http://www.meetup.com/Boston-Elixir/) that we organize. -## Want to help? ## +## Want to help? Please do! We are always looking to improve this library. Please see our [Contribution Guidelines](https://github.com/dockyard/openid_connect/blob/master/CONTRIBUTING.md) on how to properly submit issues and pull requests. -## Legal ## +## Legal [DockYard](http://dockyard.com/), Inc. © 2018 From 1e6bcd08e2feee92221a6de9d1cdeeb83760c288 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 11:46:38 -0700 Subject: [PATCH 23/32] Remove outdated configs along with the folder --- config/config.exs | 30 ------------------------------ config/dev.exs | 11 ----------- config/test.exs | 24 ------------------------ 3 files changed, 65 deletions(-) delete mode 100644 config/config.exs delete mode 100644 config/dev.exs delete mode 100644 config/test.exs diff --git a/config/config.exs b/config/config.exs deleted file mode 100644 index 23f98d3..0000000 --- a/config/config.exs +++ /dev/null @@ -1,30 +0,0 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. -import Config - -# This configuration is loaded before any dependency and is restricted -# to this project. If another project depends on this project, this -# file won't be loaded nor affect the parent project. For this reason, -# if you want to provide default values for your application for -# 3rd-party users, it should be done in your "mix.exs" file. - -# You can configure for your application as: -# -# config :openid_connect, key: :value -# -# And access this configuration in your application as: -# -# Application.get_env(:openid_connect, :key) -# -# Or configure a 3rd-party app: -# -# config :logger, level: :info -# - -# It is also possible to import configuration files, relative to this -# directory. For example, you can emulate configuration per environment -# by uncommenting the line below and defining dev.exs, test.exs and such. -# Configuration from the imported file will override the ones defined -# here (which is why it is important to import them last). -# -import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs deleted file mode 100644 index 939bb2d..0000000 --- a/config/dev.exs +++ /dev/null @@ -1,11 +0,0 @@ -import Config - -config :openid_connect, :providers, - google: [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", - client_id: "CLIENT_ID_1", - client_secret: "CLIENT_SECRET_1", - redirect_uri: "https://dev.example.com:4200/session", - scope: "openid email profile", - response_type: "code id_token token" - ] diff --git a/config/test.exs b/config/test.exs deleted file mode 100644 index b5cdc7e..0000000 --- a/config/test.exs +++ /dev/null @@ -1,24 +0,0 @@ -import Config - -config :openid_connect, :http_client, OpenIDConnect.HTTPClientMock - -config :openid_connect, :providers, [ - {"google", - [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", - client_id: "CLIENT_ID_1", - client_secret: "CLIENT_SECRET_1", - redirect_uri: "https://dev.example.com:4200/session", - scope: "openid email profile", - response_type: "code id_token token" - ]}, - {:google2, - [ - discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", - client_id: "CLIENT_ID_1", - client_secret: "CLIENT_SECRET_1", - redirect_uri: "https://dev.example.com:4200/session", - scope: "openid email profile", - response_type: "code id_token token" - ]} -] From 192c914877e7a4ae02ac9a2f1a213ec51a73e3ec Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 11:49:15 -0700 Subject: [PATCH 24/32] Fix typo in file name --- test/support/{fixutes.ex => fixtures.ex} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/support/{fixutes.ex => fixtures.ex} (100%) diff --git a/test/support/fixutes.ex b/test/support/fixtures.ex similarity index 100% rename from test/support/fixutes.ex rename to test/support/fixtures.ex From 37bb7d6ada05f51edc7e31ea7c5d68cadd924487 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 11:51:08 -0700 Subject: [PATCH 25/32] Use map instead of keyword in config example in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 502a8a3..c4660e7 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ end Most of the functions expect a `config` map which means application developer should take care for the storage of those options, eg: ```elixir -google_config = [ +google_config = %{ discovery_document_uri: "https://accounts.google.com/.well-known/openid-configuration", client_id: "CLIENT_ID", client_secret: "CLIENT_SECRET", redirect_uri: "https://example.com/session", response_type: "code", scope: "openid email profile" -] +} authorization_uri(google_config) ``` From f155b76a0a323c8a36552b2306fd0aa303dd7658 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 11:53:15 -0700 Subject: [PATCH 26/32] Fix application supervisor name --- lib/openid_connect/application.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openid_connect/application.ex b/lib/openid_connect/application.ex index f190ab1..dc5db23 100644 --- a/lib/openid_connect/application.ex +++ b/lib/openid_connect/application.ex @@ -2,7 +2,7 @@ defmodule OpenIDConnect.Application do use Application def start(_type, _args) do - opts = [strategy: :one_for_one, name: FzVpn.Supervisor] + opts = [strategy: :one_for_one, name: __MODULE__.Supervisor] Supervisor.start_link(children(), opts) end From f8b25468fc1e94f796cd9148051a6f49f1ab5558 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 11:54:20 -0700 Subject: [PATCH 27/32] Fix reference in package information --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 353ccc4..445a7af 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,7 @@ defmodule OpenIDConnect.Mixfile do deps: deps(), docs: docs(), name: "OpenID Connect", - source_url: "https://github.com/firezone/openid_connect", + source_url: "https://github.com/DockYard/openid_connect", test_coverage: [tool: ExCoveralls], preferred_cli_env: [ coveralls: :test, From e17c949a7208414c34e3d99460668b7724a35c8c Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 12:13:07 -0700 Subject: [PATCH 28/32] Add test matrix and test on all versions since 1.10 (minimal required) --- .github/workflows/test.yml | 33 ++++++++++++++++++++++----------- mix.exs | 2 +- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca4c6fc..4b872b2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,11 +11,11 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: "3.9" - uses: erlef/setup-beam@v1 with: - otp-version: '25' - elixir-version: '1.14' + otp-version: "25" + elixir-version: "1.14" - uses: actions/cache@v3.0.11 name: Setup Elixir cache with: @@ -63,23 +63,34 @@ jobs: env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + include: + - elixir-version: "1.10" + otp-version: "22" + - elixir-version: "1.11" + otp-version: "23" + - elixir-version: "1.12" + otp-version: "24" + - elixir-version: "1.13" + otp-version: "24" + - elixir-version: "1.14" + otp-version: "25" steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: - otp-version: '25' - elixir-version: '1.14' + otp-version: "${{ matrix.otp-version }}" + elixir-version: "${{ matrix.elixir-version }}" - uses: actions/cache@v3.0.11 with: path: | deps _build - key: ${{ runner.os }}-mix-otp-25-${{ hashFiles('**/mix.lock') }} + key: ${{ runner.os }}-${{ matrix.otp-version }}-${{ matrix.elixir-version }}-${{ hashFiles('**/mix.lock') }} restore-keys: | - ${{ runner.os }}-mix-otp-25- + ${{ runner.os }}-${{ matrix.otp-version }}-${{ matrix.elixir-version }}- - name: Install Dependencies run: mix deps.get --only test - - name: Run Tests and Upload Coverage Report - run: | - # XXX: This can fail when coveralls is down - mix coveralls.github + - name: Run Tests + run: mix test diff --git a/mix.exs b/mix.exs index 445a7af..237472b 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule OpenIDConnect.Mixfile do [ app: :openid_connect, version: @version, - elixir: "~> 1.13", + elixir: "~> 1.10", build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), From f782831aee066e37de6808b0145748d7fb437923 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 12:15:25 -0700 Subject: [PATCH 29/32] Use Ubuntu v20 to support older Elixir versions --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b872b2..6888328 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ on: [push, pull_request] jobs: static-analysis: - runs-on: ubuntu-latest + runs-on: ubuntu-20 env: MIX_ENV: dev GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -59,7 +59,7 @@ jobs: SKIP=no-commit-to-branch pre-commit run --all-files unit-test: - runs-on: ubuntu-latest + runs-on: ubuntu-20 env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c552c48e23626dfc89fb893fa3eccaada174e7cf Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 12:23:02 -0700 Subject: [PATCH 30/32] Change runners to avoid sitting in a long CI queue --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6888328..1c6d1ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ on: [push, pull_request] jobs: static-analysis: - runs-on: ubuntu-20 + runs-on: ubuntu-latest env: MIX_ENV: dev GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -59,7 +59,7 @@ jobs: SKIP=no-commit-to-branch pre-commit run --all-files unit-test: - runs-on: ubuntu-20 + runs-on: ubuntu-20.04 env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2d5555632ddcb6ca47fe9c8bfdb6bddd6f89c603 Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 12:26:22 -0700 Subject: [PATCH 31/32] Bump minimum required Elixir version to 1.12 because of new URI module functions used See https://github.com/elixir-lang/elixir/commit/0e81e83d1688caa7044c2b06229bbf5f09bbc489 --- .github/workflows/test.yml | 6 +----- mix.exs | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c6d1ec..bb58b7f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,17 +59,13 @@ jobs: SKIP=no-commit-to-branch pre-commit run --all-files unit-test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} strategy: matrix: include: - - elixir-version: "1.10" - otp-version: "22" - - elixir-version: "1.11" - otp-version: "23" - elixir-version: "1.12" otp-version: "24" - elixir-version: "1.13" diff --git a/mix.exs b/mix.exs index 237472b..05219c1 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule OpenIDConnect.Mixfile do [ app: :openid_connect, version: @version, - elixir: "~> 1.10", + elixir: "~> 1.12", build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), From a88b05fa1be70900219d32dfeab51cfc4e6eecdf Mon Sep 17 00:00:00 2001 From: Andrew Dryga Date: Fri, 21 Apr 2023 12:29:06 -0700 Subject: [PATCH 32/32] Add newline in the end of LICENSE.md file --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index cceb743..8c3e3ab 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -4,4 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.