diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ea79f113..b17a087179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- [#7946](https://github.com/blockscout/blockscout/pull/7946) - API v2 rate limit: Put token to cookies & change /api/v2/key method - [#7888](https://github.com/blockscout/blockscout/pull/7888) - Add token balances info to watchlist address response - [#7898](https://github.com/blockscout/blockscout/pull/7898) - Add possibility to add extra headers with JSON RPC URL - [#7836](https://github.com/blockscout/blockscout/pull/7836) - Improve unverified email flow diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index bb5f87a1dc..19182c508d 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -12,7 +12,8 @@ config :block_scout_web, cookie_domain: System.get_env("SESSION_COOKIE_DOMAIN"), # 604800 seconds, 1 week session_cookie_ttl: 60 * 60 * 24 * 7, - invalid_session_key: "invalid_session" + invalid_session_key: "invalid_session", + api_v2_temp_token_key: "api_v2_temp_token" config :block_scout_web, admin_panel_enabled: System.get_env("ADMIN_PANEL_ENABLED", "") == "true" diff --git a/apps/block_scout_web/lib/block_scout_web/api_key_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/api_key_v2_router.ex index 3a302b8a47..a1b9943f9b 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_key_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_key_v2_router.ex @@ -9,8 +9,6 @@ defmodule BlockScoutWeb.APIKeyV2Router do plug(Logger, application: :api_v2) plug(:accepts, ["json"]) plug(CheckApiV2) - plug(:fetch_session) - plug(:protect_from_forgery) end scope "/", as: :api_v2 do @@ -18,6 +16,6 @@ defmodule BlockScoutWeb.APIKeyV2Router do alias BlockScoutWeb.API.V2 - get("/", V2.APIKeyController, :get_key) + post("/", V2.APIKeyController, :get_key) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex index 9b1fc2869b..4135a6bba9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex @@ -2,10 +2,17 @@ defmodule BlockScoutWeb.API.V2.APIKeyController do use BlockScoutWeb, :controller alias BlockScoutWeb.AccessHelper - alias Plug.Crypto + + @api_v2_temp_token_key Application.compile_env(:block_scout_web, :api_v2_temp_token_key) action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(:fetch_cookies, signed: [@api_v2_temp_token_key]) + + @doc """ + Function to handle POST requests to `/api/v2/key` endpoint. It expects body with `recaptcha_response`. And puts cookie with temporary API v2 token. Which is handled here: https://github.com/blockscout/blockscout/blob/cd19739347f267d8a6ad81bbba2dbdad08bcc134/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex#L170 + """ + @spec get_key(Plug.Conn.t(), nil | map) :: {:recaptcha, any} | Plug.Conn.t() def get_key(conn, params) do helper = Application.get_env(:block_scout_web, :captcha_helper) ttl = Application.get_env(:block_scout_web, :api_rate_limit)[:api_v2_token_ttl_seconds] @@ -14,11 +21,14 @@ defmodule BlockScoutWeb.API.V2.APIKeyController do {:recaptcha, false} <- {:recaptcha, is_nil(recaptcha_response)}, {:recaptcha, true} <- {:recaptcha, helper.recaptcha_passed?(recaptcha_response)} do conn + |> put_resp_cookie(@api_v2_temp_token_key, %{ip: AccessHelper.conn_to_ip_string(conn)}, + max_age: ttl, + sign: true, + same_site: "Lax", + domain: Application.get_env(:block_scout_web, :cookie_domain) + ) |> json(%{ - key: - Crypto.sign(conn.secret_key_base, conn.secret_key_base, %{ip: AccessHelper.conn_to_ip_string(conn)}, - max_age: ttl - ) + message: "OK" }) end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex index d481945f60..d0fb8d0c57 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.AccessHelper do alias BlockScoutWeb.WebRouter.Helpers alias Explorer.AccessHelper alias Explorer.Account.Api.Key, as: ApiKey - alias Plug.{Conn, Crypto} + alias Plug.Conn alias RemoteIp @@ -77,7 +77,7 @@ defmodule BlockScoutWeb.AccessHelper do ip_string = conn_to_ip_string(conn) plan = get_plan(conn.query_params) - token = get_ui_v2_token(conn, conn.query_params, ip_string) + token = get_ui_v2_token(conn, ip_string) user_agent = get_user_agent(conn) @@ -167,18 +167,19 @@ defmodule BlockScoutWeb.AccessHelper do to_string(:inet_parse.ntoa(ip)) end - defp get_ui_v2_token(conn, %{"token" => token}, ip_string) do - case is_api_v2_request?(conn) && Crypto.verify(conn.secret_key_base, conn.secret_key_base, token) do - {:ok, %{ip: ^ip_string}} -> - token + defp get_ui_v2_token(conn, ip_string) do + api_v2_temp_token_key = Application.get_env(:block_scout_web, :api_v2_temp_token_key) + conn = Conn.fetch_cookies(conn, signed: [api_v2_temp_token_key]) + + case is_api_v2_request?(conn) && conn.cookies[api_v2_temp_token_key] do + %{ip: ^ip_string} -> + conn.req_cookies[api_v2_temp_token_key] _ -> nil end end - defp get_ui_v2_token(_conn, _params, _ip_string), do: nil - defp get_user_agent(conn) do case Conn.get_req_header(conn, "user-agent") do [agent] ->