diff --git a/config/config.exs b/config/config.exs index 1ba90df37..acffd98b6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -5,7 +5,7 @@ # is restricted to this project. # General application configuration -use Mix.Config +import Config config :arrow, ecto_repos: [Arrow.Repo], @@ -20,7 +20,10 @@ config :arrow, # map cognito groups to roles "arrow-admin" => "admin" }, + ueberauth_provider: :cognito, + api_login_module: ArrowWeb.TryApiTokenAuth.Cognito, required_roles: %{ + view_disruption: ["read-only", "admin"], create_disruption: ["admin"], update_disruption: ["admin"], delete_disruption: ["admin"], @@ -58,7 +61,10 @@ config :arrow, ArrowWeb.AuthManager, issuer: "arrow" config :ueberauth, Ueberauth, providers: [ - cognito: {Ueberauth.Strategy.Cognito, []} + cognito: {Ueberauth.Strategy.Cognito, []}, + keycloak: + {Ueberauth.Strategy.Oidcc, + issuer: :keycloak_issuer, userinfo: true, uid_field: "email", scopes: ~w"openid email"} ] config :ueberauth, Ueberauth.Strategy.Cognito, diff --git a/config/dev.exs b/config/dev.exs index 5b883fcd0..56de819a0 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # Configure your database config :arrow, Arrow.Repo, diff --git a/config/prod.exs b/config/prod.exs index 6c3ce3e0d..618230061 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # For production, don't forget to configure the url host # to something meaningful, Phoenix uses this information diff --git a/config/runtime.exs b/config/runtime.exs index f03a4d3ec..052196dce 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,5 +1,50 @@ import Config +is_test? = config_env() == :test + +keycloak_issuer = + case System.get_env() do + %{"KEYCLOAK_ISSUER" => issuer} when issuer != "" -> + issuer + + %{"KEYCLOAK_DISCOVERY_URI" => well_known} when well_known != "" -> + String.replace_trailing(well_known, "/.well-known/openid-configuration", "") + + _ -> + nil + end + +if is_binary(keycloak_issuer) and not is_test? do + config :arrow, + ueberauth_provider: :keycloak, + api_login_module: ArrowWeb.TryApiTokenAuth.Keycloak, + keycloak_client_uuid: System.fetch_env!("KEYCLOAK_CLIENT_UUID"), + keycloak_api_base: System.fetch_env!("KEYCLOAK_API_BASE") + + keycloak_opts = [ + client_id: System.fetch_env!("KEYCLOAK_CLIENT_ID"), + client_secret: System.fetch_env!("KEYCLOAK_CLIENT_SECRET") + ] + + keycloak_opts = + if keycloak_idp = System.get_env("KEYCLOAK_IDP_HINT") do + Keyword.put(keycloak_opts, :authorization_params, %{kc_idp_hint: keycloak_idp}) + else + keycloak_opts + end + + config :ueberauth_oidcc, + issuers: [ + %{ + name: :keycloak_issuer, + issuer: keycloak_issuer + } + ], + strategies: [ + keycloak: keycloak_opts + ] +end + if config_env() == :prod do sentry_env = System.get_env("SENTRY_ENV") diff --git a/config/test.exs b/config/test.exs index cdf479ef7..791957c21 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # Configure your database config :arrow, Arrow.Repo, @@ -25,7 +25,7 @@ config :arrow, http_client: Arrow.HTTPMock # Print only warnings and errors during test -config :logger, level: :warn +config :logger, level: :warning config :arrow, env: :test diff --git a/lib/arrow/adjustment_fetcher.ex b/lib/arrow/adjustment_fetcher.ex index 313915271..f740d627b 100644 --- a/lib/arrow/adjustment_fetcher.ex +++ b/lib/arrow/adjustment_fetcher.ex @@ -31,7 +31,7 @@ defmodule Arrow.AdjustmentFetcher do _ = case fetch() do :ok -> Logger.debug("adjustment_fetch_complete") - {:error, reason} -> Logger.warn("adjustment_fetch_failed: #{inspect(reason)}") + {:error, reason} -> Logger.warning("adjustment_fetch_failed: #{inspect(reason)}") end Process.send_after(self(), :fetch, interval) diff --git a/lib/arrow/permissions.ex b/lib/arrow/permissions.ex index ef29e69de..b1ff6ec3b 100644 --- a/lib/arrow/permissions.ex +++ b/lib/arrow/permissions.ex @@ -7,7 +7,8 @@ defmodule Arrow.Permissions do @required_roles Application.compile_env!(:arrow, :required_roles) @type action() :: - :create_disruption + :view_disruption + | :create_disruption | :update_disruption | :delete_disruption | :use_api diff --git a/lib/arrow_web/auth_manager/error_handler.ex b/lib/arrow_web/auth_manager/error_handler.ex index 1209542d8..4ec6b5bcf 100644 --- a/lib/arrow_web/auth_manager/error_handler.ex +++ b/lib/arrow_web/auth_manager/error_handler.ex @@ -10,6 +10,7 @@ defmodule ArrowWeb.AuthManager.ErrorHandler do @impl Guardian.Plug.ErrorHandler def auth_error(conn, {_type, _reason}, _opts) do - Controller.redirect(conn, to: Routes.auth_path(conn, :request, "cognito")) + provider = Application.get_env(:arrow, :ueberauth_provider) + Controller.redirect(conn, to: Routes.auth_path(conn, :request, "#{provider}")) end end diff --git a/lib/arrow_web/controllers/auth_controller.ex b/lib/arrow_web/controllers/auth_controller.ex index 7d95254fb..0a041c424 100644 --- a/lib/arrow_web/controllers/auth_controller.ex +++ b/lib/arrow_web/controllers/auth_controller.ex @@ -1,18 +1,28 @@ defmodule ArrowWeb.AuthController do use ArrowWeb, :controller - plug Ueberauth + plug(Ueberauth) + + @spec logout(Plug.Conn.t(), map()) :: Plug.Conn.t() + def logout(conn, _params) do + logout_url = Map.get(Guardian.Plug.current_claims(conn), "logout_url") + conn = clear_session(conn) + + if logout_url do + redirect(conn, external: logout_url) + else + redirect(conn, to: "/") + end + end @cognito_groups Application.compile_env!(:arrow, :cognito_groups) @spec callback(Plug.Conn.t(), map()) :: Plug.Conn.t() - def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do + def callback(%{assigns: %{ueberauth_auth: %{provider: :cognito} = auth}} = conn, _params) do username = auth.uid expiration = auth.credentials.expires_at - credentials = conn.assigns.ueberauth_auth.credentials - current_time = System.system_time(:second) - groups = credentials.other[:groups] || [] + groups = Map.get(auth.credentials.other, :groups, []) roles = Enum.flat_map(groups, fn group -> @@ -26,10 +36,43 @@ defmodule ArrowWeb.AuthController do |> Guardian.Plug.sign_in( ArrowWeb.AuthManager, username, - %{roles: roles}, + %{ + # all cognito users have read-only access + roles: roles ++ ["read-only"] + }, + ttl: {expiration - current_time, :seconds} + ) + |> redirect(to: Routes.disruption_path(conn, :index)) + end + + def callback(%{assigns: %{ueberauth_auth: %{provider: :keycloak} = auth}} = conn, _params) do + username = auth.uid + expiration = auth.credentials.expires_at + current_time = System.system_time(:second) + + roles = auth.extra.raw_info.userinfo["roles"] || [] + + logout_url = + case UeberauthOidcc.initiate_logout_url(auth, %{ + post_logout_redirect_uri: "https://www.mbta.com/" + }) do + {:ok, url} -> + url + + _ -> + nil + end + + conn + |> Guardian.Plug.sign_in( + ArrowWeb.AuthManager, + username, + %{ + roles: roles, + logout_url: logout_url + }, ttl: {expiration - current_time, :seconds} ) - |> put_session(:arrow_username, username) |> redirect(to: Routes.disruption_path(conn, :index)) end diff --git a/lib/arrow_web/controllers/disruption_controller.ex b/lib/arrow_web/controllers/disruption_controller.ex index fd8c7ff05..811680add 100644 --- a/lib/arrow_web/controllers/disruption_controller.ex +++ b/lib/arrow_web/controllers/disruption_controller.ex @@ -8,6 +8,7 @@ defmodule ArrowWeb.DisruptionController do alias Ecto.Changeset alias Plug.Conn + plug(Authorize, :view_disruption when action in [:index, :show]) plug(Authorize, :create_disruption when action in [:new, :create]) plug(Authorize, :update_disruption when action in [:edit, :update, :update_row_status]) plug(Authorize, :delete_disruption when action in [:delete]) diff --git a/lib/arrow_web/controllers/my_token_controller.ex b/lib/arrow_web/controllers/my_token_controller.ex index edbba5960..3e059ab7b 100644 --- a/lib/arrow_web/controllers/my_token_controller.ex +++ b/lib/arrow_web/controllers/my_token_controller.ex @@ -6,7 +6,7 @@ defmodule ArrowWeb.MyTokenController do @spec show(Plug.Conn.t(), Plug.Conn.params()) :: Plug.Conn.t() def show(conn, _params) do - token = conn |> get_session(:arrow_username) |> AuthToken.get_or_create_token_for_user() + token = conn |> Guardian.Plug.current_resource() |> AuthToken.get_or_create_token_for_user() render(conn, "index.html", token: token) end diff --git a/lib/arrow_web/controllers/unauthorized_controller.ex b/lib/arrow_web/controllers/unauthorized_controller.ex index f8e46487a..846a9343f 100644 --- a/lib/arrow_web/controllers/unauthorized_controller.ex +++ b/lib/arrow_web/controllers/unauthorized_controller.ex @@ -3,7 +3,10 @@ defmodule ArrowWeb.UnauthorizedController do @spec index(Plug.Conn.t(), map()) :: Plug.Conn.t() def index(conn, _params) do + roles = Guardian.Plug.current_claims(conn)["roles"] || [] + conn + |> assign(:roles, roles) |> put_status(403) |> render("index.html") end diff --git a/lib/arrow_web/router.ex b/lib/arrow_web/router.ex index 195ce9f3e..34e39a506 100644 --- a/lib/arrow_web/router.ex +++ b/lib/arrow_web/router.ex @@ -11,7 +11,6 @@ defmodule ArrowWeb.Router do pipeline :json_api do plug(:accepts, ["json-api"]) - plug(:fetch_session) plug(JaSerializer.ContentTypeNegotiation) end @@ -42,6 +41,7 @@ defmodule ArrowWeb.Router do scope "/", ArrowWeb do pipe_through([:redirect_prod_http, :browser, :authenticate]) + get("/logout", AuthController, :logout) get("/unauthorized", UnauthorizedController, :index) get("/feed", FeedController, :index) get("/mytoken", MyTokenController, :show) diff --git a/lib/arrow_web/templates/layout/app.html.heex b/lib/arrow_web/templates/layout/app.html.heex index 71a619263..e189b5c33 100644 --- a/lib/arrow_web/templates/layout/app.html.heex +++ b/lib/arrow_web/templates/layout/app.html.heex @@ -27,6 +27,11 @@ diff --git a/lib/arrow_web/templates/unauthorized/index.html.heex b/lib/arrow_web/templates/unauthorized/index.html.heex index 5a25e3abc..ac0926f14 100644 --- a/lib/arrow_web/templates/unauthorized/index.html.heex +++ b/lib/arrow_web/templates/unauthorized/index.html.heex @@ -1,8 +1,10 @@

Whoops! You are not authorized to access this page.

+ <%= if "read-only" in @roles or "admin" in @roles do %> Were you looking for a <%= link("list of disruptions", to: Routes.disruption_path(@conn, :index)) %> or a <%= link("calendar schedule", to: Routes.disruption_path(@conn, :index, view: "calendar")) %>? + <% end %> To request access to this page, please contact <%= link("trc@mbta.com", to: "mailto:trc@mbta.com") %>.
diff --git a/lib/arrow_web/try_api_token_auth.ex b/lib/arrow_web/try_api_token_auth.ex index 765f1ad46..29a872bcc 100644 --- a/lib/arrow_web/try_api_token_auth.ex +++ b/lib/arrow_web/try_api_token_auth.ex @@ -6,72 +6,26 @@ defmodule ArrowWeb.TryApiTokenAuth do import Plug.Conn require Logger - @aws_cognito_target "AWSCognitoIdentityProviderService" - @cognito_groups Application.compile_env!(:arrow, :cognito_groups) - def init(options), do: options def call(conn, _opts) do api_key_values = get_req_header(conn, "x-api-key") - if api_key_values == [] do + with [token | _] <- api_key_values, + token = String.downcase(token), + auth_token = %Arrow.AuthToken{} <- + Arrow.Repo.get_by(Arrow.AuthToken, token: token), + api_login_module = Application.get_env(:arrow, :api_login_module), + conn = api_login_module.sign_in(conn, auth_token), + true <- Guardian.Plug.authenticated?(conn) do conn else - [token | _] = api_key_values - token = String.downcase(token) - - auth_token = Arrow.Repo.get_by(Arrow.AuthToken, token: token) + [] -> + # no API key present, pass on through + conn - if is_nil(auth_token) do + _ -> conn |> send_resp(401, "unauthenticated") |> halt() - else - user_pool_id = - :ueberauth - |> Application.get_env(Ueberauth.Strategy.Cognito) - |> Keyword.get(:user_pool_id) - |> config_value - - data = %{ - "Username" => auth_token.username, - "UserPoolId" => user_pool_id - } - - headers = [ - {"x-amz-target", "#{@aws_cognito_target}.AdminListGroupsForUser"}, - {"content-type", "application/x-amz-json-1.1"} - ] - - operation = ExAws.Operation.JSON.new(:"cognito-idp", data: data, headers: headers) - - {module, function} = Application.get_env(:arrow, :ex_aws_requester) - - roles = - case apply(module, function, [operation]) do - {:ok, %{"Groups" => groups}} -> - Enum.flat_map(groups, fn %{"GroupName" => group} -> - case @cognito_groups[group] do - role when is_binary(role) -> [role] - _ -> [] - end - end) - - response -> - :ok = Logger.warn("unexpected_aws_api_response: #{inspect(response)}") - [] - end - - conn - |> Guardian.Plug.sign_in( - ArrowWeb.AuthManager, - auth_token.username, - %{roles: roles} - ) - |> put_session(:arrow_username, auth_token.username) - end end end - - @spec config_value(binary() | {module(), atom(), [any()]}) :: any() - defp config_value(value) when is_binary(value), do: value - defp config_value({m, f, a}), do: apply(m, f, a) end diff --git a/lib/arrow_web/try_api_token_auth/cognito.ex b/lib/arrow_web/try_api_token_auth/cognito.ex new file mode 100644 index 000000000..73188dfd0 --- /dev/null +++ b/lib/arrow_web/try_api_token_auth/cognito.ex @@ -0,0 +1,57 @@ +defmodule ArrowWeb.TryApiTokenAuth.Cognito do + @moduledoc """ + Signs in an API client via Cognito. + """ + + require Logger + + @aws_cognito_target "AWSCognitoIdentityProviderService" + @cognito_groups Application.compile_env!(:arrow, :cognito_groups) + + def sign_in(conn, auth_token) do + user_pool_id = + :ueberauth + |> Application.get_env(Ueberauth.Strategy.Cognito) + |> Keyword.get(:user_pool_id) + |> config_value + + data = %{ + "Username" => auth_token.username, + "UserPoolId" => user_pool_id + } + + headers = [ + {"x-amz-target", "#{@aws_cognito_target}.AdminListGroupsForUser"}, + {"content-type", "application/x-amz-json-1.1"} + ] + + operation = ExAws.Operation.JSON.new(:"cognito-idp", data: data, headers: headers) + + {module, function} = Application.get_env(:arrow, :ex_aws_requester) + + roles = + case apply(module, function, [operation]) do + {:ok, %{"Groups" => groups}} -> + for %{"GroupName" => group} <- groups, + {:ok, role} <- [Map.fetch(@cognito_groups, group)] do + role + end + + response -> + :ok = Logger.warn("unexpected_aws_api_response: #{inspect(response)}") + [] + end + + conn + |> Guardian.Plug.sign_in( + ArrowWeb.AuthManager, + auth_token.username, + %{roles: roles}, + ttl: {0, :second} + ) + end + + @spec config_value(binary() | {module(), atom(), [any()]}) :: any() + defp config_value(value) when is_binary(value), do: value + defp config_value({m, f, a}), do: apply(m, f, a) +end diff --git a/lib/arrow_web/try_api_token_auth/keycloak.ex b/lib/arrow_web/try_api_token_auth/keycloak.ex new file mode 100644 index 000000000..2189b0de2 --- /dev/null +++ b/lib/arrow_web/try_api_token_auth/keycloak.ex @@ -0,0 +1,96 @@ +defmodule ArrowWeb.TryApiTokenAuth.Keycloak do + @moduledoc """ + Signs in an API client via Keycloak. + """ + + require Logger + + def sign_in(conn, auth_token) do + with {:ok, user_id} <- lookup_user_id(auth_token.username), + {:ok, roles} <- lookup_user_roles(user_id) do + conn + |> Guardian.Plug.sign_in( + ArrowWeb.AuthManager, + auth_token.username, + %{roles: roles}, + ttl: {0, :second} + ) + else + other -> + Logger.warn( + "unexpected response when logging #{auth_token.username} in via Keycloak API: #{inspect(other)}" + ) + + conn + end + end + + defp lookup_user_id(email) do + case keycloak_api("/users", %{ + max: 1, + email: String.downcase(email), + exact: true, + briefRepresentation: true + }) do + {:ok, [%{"id" => user_id}]} -> + {:ok, user_id} + + {:ok, []} -> + {:error, :no_users} + + {:ok, [_, _ | _]} -> + {:error, :multiple_users} + + e -> + e + end + end + + defp lookup_user_roles(user_id) do + client_uuid = Application.get_env(:arrow, :keycloak_client_uuid) + url = "/users/#{user_id}/role-mappings/clients/#{client_uuid}/composite" + + case keycloak_api(url) do + {:ok, response} -> + roles = for r <- response, do: r["name"] + {:ok, roles} + + e -> + e + end + end + + defp keycloak_api(url, params \\ %{}) do + base_url = Application.get_env(:arrow, :keycloak_api_base) + {_, base_opts} = Application.get_env(:ueberauth, Ueberauth)[:providers][:keycloak] + runtime_opts = Application.get_env(:ueberauth_oidcc, :strategies)[:keycloak] + + opts = + base_opts + |> Keyword.merge(runtime_opts) + |> Map.new() + + with {:ok, token} <- + Oidcc.client_credentials_token(opts.issuer, opts.client_id, opts.client_secret, %{}), + headers = [{"authorization", "Bearer #{token.access.token}"}], + {:ok, %{status_code: 200} = response} <- + HTTPoison.get("#{base_url}#{url}", headers, + params: params, + hackney: [ + ssl_options: [ + verify: :verify_peer, + cacerts: :public_key.cacerts_get(), + versions: [:"tlsv1.2"], + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ] + ] + ] + ) do + Jason.decode(response.body) + else + {:ok, %{status_code: _} = response} -> {:error, response} + e -> e + end + end +end diff --git a/mix.exs b/mix.exs index 802f7e89b..bcc1211d2 100644 --- a/mix.exs +++ b/mix.exs @@ -63,21 +63,21 @@ defmodule Arrow.MixProject do {:phoenix_ecto, "~> 4.0"}, # override for react_phoenix, pending # https://github.com/geolessel/react-phoenix/pull/58 - {:phoenix_html, "~> 3.0", override: true}, + {:phoenix_html, "~> 3.2.0", override: true}, {:phoenix_live_reload, "~> 1.2", only: :dev}, - {:phoenix_live_view, "~> 0.20.1"}, + {:phoenix_live_view, "~> 0.16.4"}, {:phoenix_pubsub, "~> 2.0"}, {:phoenix, "~> 1.6.0"}, {:plug_cowboy, "~> 2.1"}, + {:telemetry, "~> 1.2", override: true}, {:postgrex, ">= 0.0.0"}, # If react_phoenix changes, check assets/src/ReactPhoenix.js, too {:react_phoenix, "1.2.0"}, - {:telemetry_metrics, "~> 0.6"}, - {:telemetry_poller, "~> 0.5"}, {:tzdata, "~> 1.1"}, {:ueberauth_cognito, "0.4.0"}, + {:ueberauth_oidcc, "~> 0.1.0-rc"}, {:ueberauth, "~> 0.9"}, - {:wallaby, "~> 0.28.1", runtime: false, only: :test}, + {:wallaby, "~> 0.30.6", runtime: false, only: :test}, {:sentry, "~> 8.0"} ] end diff --git a/mix.lock b/mix.lock index aa2b641ba..f96e6ea5c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,10 +1,10 @@ %{ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "castore": {:hex, :castore, "0.1.17", "ba672681de4e51ed8ec1f74ed624d104c0db72742ea1a5e74edbc770c815182f", [:mix], [], "hexpm", "d9844227ed52d26e7519224525cb6868650c272d4a3d327ce3ca5570c12163f9"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, - "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, + "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.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [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", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, @@ -20,30 +20,37 @@ "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, "expo": {:hex, :expo, "0.1.0", "d4e932bdad052c374118e312e35280f1919ac13881cb3ac07a209a54d0c81dd8", [:mix], [], "hexpm", "c22c536021c56de058aaeedeabb4744eb5d48137bacf8c29f04d25b6c6bbbf45"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [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 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, "gettext": {:hex, :gettext, "0.21.0", "15bbceb20b317b706a8041061a08e858b5a189654128618b53746bf36c84352b", [:mix], [{:expo, "~> 0.1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "04a66db4103b6d1d18f92240bb2c73167b517229316b7bef84e4eebbfb2f14f6"}, "guardian": {:hex, :guardian, "2.3.1", "2b2d78dc399a7df182d739ddc0e566d88723299bfac20be36255e2d052fd215d", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bbe241f9ca1b09fad916ad42d6049d2600bbc688aba5b3c4a6c82592a54274c3"}, "guardian_phoenix": {:hex, :guardian_phoenix, "2.0.1", "89a817265af09a6ddf7cb1e77f17ffca90cea2db10ff888375ef34502b2731b1", [:mix], [{:guardian, "~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "21f439246715192b231f228680465d1ed5fbdf01555a4a3b17165532f5f9a08c"}, - "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.1", "df030d96de89dad2e9983f92b0c506a642d4b1f4a819c96ff77d12796189c63e", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "35156a6d678d6d516b9229e208942c405cf21232edd632327ecfaf4fd03e79e0"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.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.4.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", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "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"}, "inflex": {:hex, :inflex, "1.10.0", "8366a7696e70e1813aca102e61274addf85d99f4a072b2f9c7984054ea1b9d29", [:mix], [], "hexpm", "7b5ccb9b720c26516f5962dc4565fc26f083ca107b0f6c167048506a125d2df3"}, "ja_serializer": {:git, "https://github.com/mbta/ja_serializer.git", "efb1d4489809e31e4b54b4af9e85f0b3ceeb650b", [branch: "master"]}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"}, + "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, "lcov_ex": {:hex, :lcov_ex, "0.2.2", "2dd1ef86ed510ac9c3fe3f11b82cbfd09c92b91b7d6565298932c560f72bd584", [:mix], [], "hexpm", "5e412eddb6a384d2e66cff94eec048ba5c9ed30d6cd9fb5a331456b210de0801"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, "mox": {:hex, :mox, "1.0.1", "b651bf0113265cda0ba3a827fcb691f848b683c373b77e7d7439910a8d754d6e", [:mix], [], "hexpm", "35bc0dea5499d18db4ef7fe4360067a59b06c74376eb6ab3bd67e6295b133469"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, - "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, + "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, + "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "oauth2": {:hex, :oauth2, "2.0.1", "70729503e05378697b958919bb2d65b002ba6b28c8112328063648a9348aaa3f", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "c64e20d4d105bcdbcbe03170fb530d0eddc3a3e6b135a87528a22c8aecf74c52"}, + "oidcc": {:hex, :oidcc, "3.1.0-beta.2", "3a3f4d9cf9026392579684adfdd903e76b2cc7634a0c443fa477b940f31c3b83", [:mix, :rebar3], [{:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.1", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "65f12afb4d0afa9f121ba419dcc88251163dd0553309b2e13b8edf217acda5c4"}, + "openid_connect": {:git, "https://github.com/firezone/openid_connect.git", "13320ed8b0d347330d07e1375a9661f3089b9c03", [ref: "13320ed8b0d347330d07e1375a9661f3089b9c03"]}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "phoenix": {:hex, :phoenix, "1.6.9", "648e660040cdc758c5401972e0f592ce622d4ce9cd16d2d9c33dda32d0c9f7fa", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be2fe497597d6bf297dcbf9f4416b4929dbfbdcc25edc1acf6d4dcaecbe898a6"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, + "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.1", "92a37acf07afca67ac98bd326532ba8f44ad7d4bdf3e4361b03f7f02594e5ae9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be494fd1215052729298b0e97d5c2ce8e719c00854b82cd8cf15c1cd7fcf6294"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"}, - "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.16.4", "5692edd0bac247a9a816eee7394e32e7a764959c7d0cf9190662fc8b0cd24c97", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "754ba49aa2e8601afd4f151492c93eb72df69b0b9856bab17711b8397e43bba0"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, + "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [: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", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, @@ -52,15 +59,19 @@ "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "react_phoenix": {:hex, :react_phoenix, "1.2.0", "42f4f6a7d1006b50f89f2209fc1402e8d6b5cca34ca0fbfcdc0c43619db1ad4a", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.11", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "aab3a7ba35e68776da5d52817b1044b130e13740b72db9faa8a919dfce68be66"}, "sentry": {:hex, :sentry, "8.0.6", "c8de1bf0523bc120ec37d596c55260901029ecb0994e7075b0973328779ceef7", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "051a2d0472162f3137787c7c9d6e6e4ef239de9329c8c45b1f1bf1e9379e1883"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"}, - "tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"}, + "telemetry_registry": {:hex, :telemetry_registry, "0.3.1", "14a3319a7d9027bdbff7ebcacf1a438f5f5c903057b93aee484cca26f05bdcba", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d0ca77b691cf854ed074b459a93b87f4c7f5512f8f7743c635ca83da81f939e"}, + "tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, - "ueberauth": {:hex, :ueberauth, "0.9.0", "9f2dc8f6158fc09d048da0c1a548a4b2f9326bf01a35acdcaa94f4bc5b936c9a", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6d6e0c6f7191b8d25153ae3596b3d98b5c06f9bb887d1e2d7b98b74eff3d189b"}, + "ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"}, "ueberauth_cognito": {:hex, :ueberauth_cognito, "0.4.0", "62daa3f675298c2b03002d2e1b7e5a30cbc513400e5732a264864a26847e71ac", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.0", [hex: :jose, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "62378f4f34c8569cd95cc4e7463c56e9981c8afc83fdc516922065f0e1302a35"}, + "ueberauth_keycloak_strategy": {:hex, :ueberauth_keycloak_strategy, "0.4.0", "51e975874564ef4a6eb0044b9f0c6a08be4ba6086e62e41d385e7dd52fe9568b", [:mix], [{:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "c03027937bddcbd9ff499e457f9bb05f79018fa321abf79ebcfed2af0007211b"}, + "ueberauth_oidc": {:git, "https://github.com/mbta/ueberauth_oidc.git", "6216cb2a93bf075b76bcd97db89579233c44314f", []}, + "ueberauth_oidcc": {:hex, :ueberauth_oidcc, "0.1.0-rc.0", "6bf1404e35cf919f27e774ca990255725fdc4ea6198021b752d0244e4df74ef2", [:mix], [{:oidcc, "~> 3.1.0-beta", [hex: :oidcc, repo: "hexpm", optional: false]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.10.5", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "319cd35580373e146b9f707ebbd6b86785b40e5136f7427e0af64abe5183e111"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, - "wallaby": {:hex, :wallaby, "0.28.1", "0487ac4e76a5ffcc9b0ac3ddc35b931b0c2f4cac87b30b029a0f4e7e5ee20ff3", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.1.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "618538448e21dc8b0a02f6810472eb48a05badf14db4a0441dc562a1eac896e9"}, - "web_driver_client": {:hex, :web_driver_client, "0.1.0", "19466a989c76b7ec803c796cec0fec4611a64f445fd5120ce50c9e3817e09c2c", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "c9c031ca915e8fc75b5e24ac93503244f3cc406dd7f53047087a45aa62d60e9e"}, + "wallaby": {:hex, :wallaby, "0.30.6", "7dc4c1213f3b52c4152581d126632bc7e06892336d3a0f582853efeeabd45a71", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "50950c1d968549b54c20e16175c68c7fc0824138e2bb93feb11ef6add8eb23d4"}, + "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, } diff --git a/test/arrow_web/auth_manager/error_handler_test.exs b/test/arrow_web/auth_manager/error_handler_test.exs index 31dd3504c..bc63fb93e 100644 --- a/test/arrow_web/auth_manager/error_handler_test.exs +++ b/test/arrow_web/auth_manager/error_handler_test.exs @@ -2,13 +2,15 @@ defmodule ArrowWeb.AuthManager.ErrorHandlerTest do use ArrowWeb.ConnCase describe "auth_error/3" do - test "redirects to Cognito login if there's no refresh key", %{conn: conn} do + test "redirects to login if there's no refresh key", %{conn: conn} do + provider = Application.get_env(:arrow, :ueberauth_provider) + conn = conn |> init_test_session(%{}) |> ArrowWeb.AuthManager.ErrorHandler.auth_error({:some_type, :reason}, []) - assert html_response(conn, 302) =~ "\"/auth/cognito\"" + assert html_response(conn, 302) =~ "\"/auth/#{provider}\"" end end end diff --git a/test/arrow_web/controllers/auth_controller_test.exs b/test/arrow_web/controllers/auth_controller_test.exs index 735a13342..b5ad22007 100644 --- a/test/arrow_web/controllers/auth_controller_test.exs +++ b/test/arrow_web/controllers/auth_controller_test.exs @@ -2,11 +2,12 @@ defmodule ArrowWeb.Controllers.AuthControllerTest do use ArrowWeb.ConnCase describe "callback" do - test "redirects on success", %{conn: conn} do + test "redirects on success (cognito)", %{conn: conn} do current_time = System.system_time(:second) auth = %Ueberauth.Auth{ uid: "foo@mbta.com", + provider: :cognito, credentials: %Ueberauth.Auth.Credentials{ expires_at: current_time + 1_000, other: %{groups: ["arrow-admin"]} @@ -20,9 +21,69 @@ defmodule ArrowWeb.Controllers.AuthControllerTest do response = html_response(conn, 302) + assert response =~ Routes.disruption_path(conn, :index) + assert Enum.sort(Guardian.Plug.current_claims(conn)["roles"]) == ["admin", "read-only"] + assert Guardian.Plug.current_resource(conn) == "foo@mbta.com" + end + + test "redirects on success (keycloak)", %{conn: conn} do + current_time = System.system_time(:second) + + auth = %Ueberauth.Auth{ + uid: "foo@mbta.com", + provider: :keycloak, + credentials: %Ueberauth.Auth.Credentials{ + expires_at: current_time + 1_000, + other: %{id_token: "id_token"} + }, + extra: %{ + raw_info: %{ + userinfo: %{ + "roles" => ["admin"] + } + } + } + } + + conn = + conn + |> assign(:ueberauth_auth, auth) + |> get(Routes.auth_path(conn, :callback, "keycloak")) + + response = html_response(conn, 302) + assert response =~ Routes.disruption_path(conn, :index) assert Guardian.Plug.current_claims(conn)["roles"] == ["admin"] - assert get_session(conn, :arrow_username) == "foo@mbta.com" + assert Guardian.Plug.current_resource(conn) == "foo@mbta.com" + end + + test "handles missing roles (keycloak)", %{conn: conn} do + current_time = System.system_time(:second) + + auth = %Ueberauth.Auth{ + uid: "foo@mbta.com", + provider: :keycloak, + credentials: %Ueberauth.Auth.Credentials{ + expires_at: current_time + 1_000, + other: %{id_token: "id_token"} + }, + extra: %{ + raw_info: %{ + userinfo: %{} + } + } + } + + conn = + conn + |> assign(:ueberauth_auth, auth) + |> get(Routes.auth_path(conn, :callback, "keycloak")) + + response = html_response(conn, 302) + + assert response =~ Routes.disruption_path(conn, :index) + assert Guardian.Plug.current_claims(conn)["roles"] == [] + assert Guardian.Plug.current_resource(conn) == "foo@mbta.com" end test "handles generic failure", %{conn: conn} do diff --git a/test/arrow_web/controllers/unauthorized_controller_test.exs b/test/arrow_web/controllers/unauthorized_controller_test.exs index e66b9301e..96deebe67 100644 --- a/test/arrow_web/controllers/unauthorized_controller_test.exs +++ b/test/arrow_web/controllers/unauthorized_controller_test.exs @@ -5,8 +5,17 @@ defmodule ArrowWeb.UnauthorizedControllerTest do @tag :authenticated test "renders response", %{conn: conn} do conn = get(conn, Routes.unauthorized_path(conn, :index)) + response = html_response(conn, 403) + assert response =~ "not authorized" + assert response =~ "calendar schedule" + end - assert html_response(conn, 403) =~ "not authorized" + @tag :authenticated_empty + test "does not offer a calendar if the user has no roles", %{conn: conn} do + conn = get(conn, Routes.unauthorized_path(conn, :index)) + response = html_response(conn, 403) + assert response =~ "not authorized" + refute response =~ "calendar schedule" end end end diff --git a/test/arrow_web/plug/assign_user_test.exs b/test/arrow_web/plug/assign_user_test.exs index 20dfe37a0..53b49f92a 100644 --- a/test/arrow_web/plug/assign_user_test.exs +++ b/test/arrow_web/plug/assign_user_test.exs @@ -18,9 +18,9 @@ defmodule ArrowWeb.Plug.AssignUserTest do end @tag :authenticated - test "loads a non-admin into the connection when user has no roles", %{conn: conn} do + test "loads a non-admin into the connection when user is not an admin", %{conn: conn} do assert AssignUser.call(conn, []).assigns == %{ - current_user: %User{id: "test_user", roles: MapSet.new()} + current_user: %User{id: "test_user", roles: MapSet.new(["read-only"])} } end end diff --git a/test/arrow_web/try_api_token_auth_test.exs b/test/arrow_web/try_api_token_auth_test.exs index 6a77ca0e8..2ce1bef58 100644 --- a/test/arrow_web/try_api_token_auth_test.exs +++ b/test/arrow_web/try_api_token_auth_test.exs @@ -53,7 +53,7 @@ defmodule ArrowWeb.TryApiTokenAuthTest do assert claims["sub"] == "foo@mbta.com" assert claims["typ"] == "access" assert claims["roles"] == ["admin"] - assert get_session(conn, :arrow_username) == "foo@mbta.com" + assert Guardian.Plug.current_resource(conn) == "foo@mbta.com" end test "handles unexpected response from Cognito API", %{conn: conn} do diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 08608ad04..dfc625684 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -39,11 +39,14 @@ defmodule ArrowWeb.ConnCase do cond do tags[:authenticated] -> - {:ok, conn: build_conn("test_user")} + {:ok, conn: build_conn("test_user", ["read-only"])} tags[:authenticated_admin] -> {:ok, conn: build_conn("test_user", ["admin"])} + tags[:authenticated_empty] -> + {:ok, conn: build_conn("test_user", [])} + true -> {:ok, conn: @@ -53,10 +56,10 @@ defmodule ArrowWeb.ConnCase do end @spec build_conn(String.t(), [String.t()] | []) :: Plug.Conn.t() - defp build_conn(user, roles \\ []) do + defp build_conn(user, roles) do Phoenix.ConnTest.build_conn() |> Plug.Conn.put_req_header("x-forwarded-proto", "https") - |> init_test_session(%{arrow_username: user}) + |> init_test_session(%{}) |> Guardian.Plug.sign_in(ArrowWeb.AuthManager, user, %{roles: roles}) end end