diff --git a/apps/boruta_identity/lib/boruta_identity_web/concerns/authenticable.ex b/apps/boruta_identity/lib/boruta_identity_web/concerns/authenticable.ex index 7ad1cc9d4..afb923a4a 100644 --- a/apps/boruta_identity/lib/boruta_identity_web/concerns/authenticable.ex +++ b/apps/boruta_identity/lib/boruta_identity_web/concerns/authenticable.ex @@ -9,7 +9,6 @@ defmodule BorutaIdentityWeb.Authenticable do # Make the remember me cookie valid for 60 days. # If you want bump or reduce this value, also change # the token expiry itself in UserToken. - @session_key :user_token @max_age 60 * 60 * 24 * 60 @remember_me_cookie "_boruta_identity_web_user_remember_me" @remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"] @@ -17,6 +16,11 @@ defmodule BorutaIdentityWeb.Authenticable do @spec remember_me_cookie() :: String.t() def remember_me_cookie, do: @remember_me_cookie + @spec session_key(conn :: Plug.Conn.t()) :: String.t() + def session_key(conn) do + client_id_from_request(conn) + end + @spec store_user_session(conn :: Plug.Conn.t(), session_token :: String.t()) :: conn :: Plug.Conn.t() def store_user_session(%Plug.Conn{body_params: params} = conn, session_token) do @@ -24,14 +28,13 @@ defmodule BorutaIdentityWeb.Authenticable do conn |> assign(:current_user, user) - |> put_session(@session_key, session_token) - |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(session_token)}") + |> put_session(session_key(conn), session_token) |> maybe_write_remember_me_cookie(session_token, params["user"]) end @spec get_user_session(conn :: Plug.Conn.t()) :: session_token :: String.t() def get_user_session(conn) do - get_session(conn, @session_key) + get_session(conn, session_key(conn)) end @spec remove_user_session(conn :: Plug.Conn.t()) :: conn :: Plug.Conn.t() @@ -42,7 +45,7 @@ defmodule BorutaIdentityWeb.Authenticable do end defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => remember_me}) - when remember_me in ["true", "on"] do + when remember_me in ["true", "on", true] do put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options) end @@ -103,16 +106,24 @@ defmodule BorutaIdentityWeb.Authenticable do end @spec client_id_from_request(conn :: Plug.Conn.t()) :: String.t() | nil - def client_id_from_request(%Plug.Conn{query_params: query_params}) do - with {:ok, claims} <- - BorutaIdentityWeb.Token.verify( - query_params["request"] || "", - BorutaIdentityWeb.Token.application_signer() - ), - {:ok, client_id} <- Map.fetch(claims, "client_id") do - client_id - else - _ -> nil + def client_id_from_request(conn) do + %Plug.Conn{query_params: query_params} = fetch_query_params(conn) + + case query_params do + %{"client_id" => client_id} -> + client_id + + %{"request" => request} -> + with {:ok, claims} <- + BorutaIdentityWeb.Token.verify( + request || "", + BorutaIdentityWeb.Token.application_signer() + ), + {:ok, client_id} <- Map.fetch(claims, "client_id") do + client_id + else + _ -> nil + end end end diff --git a/apps/boruta_identity/lib/boruta_identity_web/plugs/sessions.ex b/apps/boruta_identity/lib/boruta_identity_web/plugs/sessions.ex index 615b36dd6..24b9ac7f8 100644 --- a/apps/boruta_identity/lib/boruta_identity_web/plugs/sessions.ex +++ b/apps/boruta_identity/lib/boruta_identity_web/plugs/sessions.ex @@ -3,7 +3,7 @@ defmodule BorutaIdentityWeb.Sessions do use BorutaIdentityWeb, :controller - import BorutaIdentityWeb.Authenticable, only: [remember_me_cookie: 0, after_sign_in_path: 1] + import BorutaIdentityWeb.Authenticable, only: [session_key: 1, remember_me_cookie: 0, after_sign_in_path: 1] alias BorutaIdentity.Accounts @@ -19,13 +19,13 @@ defmodule BorutaIdentityWeb.Sessions do end defp ensure_user_token(conn) do - if user_token = get_session(conn, :user_token) do + if user_token = get_session(conn, session_key(conn)) do {user_token, conn} else conn = fetch_cookies(conn, signed: [remember_me_cookie()]) if user_token = conn.cookies[remember_me_cookie()] do - {user_token, put_session(conn, :user_token, user_token)} + {user_token, put_session(conn, session_key(conn), user_token)} else {nil, conn} end diff --git a/apps/boruta_identity/test/boruta_identity_web/controllers/user_registration_controller_test.exs b/apps/boruta_identity/test/boruta_identity_web/controllers/user_registration_controller_test.exs index ff1d7146e..3a30bee34 100644 --- a/apps/boruta_identity/test/boruta_identity_web/controllers/user_registration_controller_test.exs +++ b/apps/boruta_identity/test/boruta_identity_web/controllers/user_registration_controller_test.exs @@ -2,6 +2,7 @@ defmodule BorutaIdentityWeb.UserRegistrationControllerTest do use BorutaIdentityWeb.ConnCase, async: true import BorutaIdentity.AccountsFixtures + import BorutaIdentityWeb.Authenticable, only: [session_key: 1] alias BorutaIdentity.Repo @@ -51,7 +52,7 @@ defmodule BorutaIdentityWeb.UserRegistrationControllerTest do "user" => %{"email" => email, "password" => valid_user_password()} }) - assert get_session(conn, :user_token) + assert get_session(conn, session_key(conn)) assert redirected_to(conn) =~ "/user_return_to" end diff --git a/apps/boruta_identity/test/boruta_identity_web/controllers/user_session_controller_test.exs b/apps/boruta_identity/test/boruta_identity_web/controllers/user_session_controller_test.exs index 678988c8f..9ec32f826 100644 --- a/apps/boruta_identity/test/boruta_identity_web/controllers/user_session_controller_test.exs +++ b/apps/boruta_identity/test/boruta_identity_web/controllers/user_session_controller_test.exs @@ -2,6 +2,7 @@ defmodule BorutaIdentityWeb.UserSessionControllerTest do use BorutaIdentityWeb.ConnCase, async: true import BorutaIdentity.AccountsFixtures + import BorutaIdentityWeb.Authenticable, only: [session_key: 1] alias BorutaIdentity.Repo @@ -24,26 +25,20 @@ defmodule BorutaIdentityWeb.UserSessionControllerTest do end test "redirects if already logged in", %{conn: conn, user: user, request: request} do - conn = conn |> log_in(user) |> get(Routes.user_session_path(conn, :new, request: request)) - assert redirected_to(conn) == "/user_return_to" - end - end - - describe "POST /users/log_in" do - test "logs the user in with remember me", %{conn: conn, user: user, request: request} do conn = post(conn, Routes.user_session_path(conn, :create, request: request), %{ "user" => %{ "email" => user.username, - "password" => valid_user_password(), - "remember_me" => "true" + "password" => valid_user_password() } }) - assert conn.resp_cookies["_boruta_identity_web_user_remember_me"] - assert redirected_to(conn) =~ "/" + conn = conn |> get(Routes.user_session_path(conn, :new, request: request)) + assert redirected_to(conn) == "/user_return_to" end + end + describe "POST /users/log_in" do test "returns unauthorized when not confirmed", %{ conn: conn, request: request, @@ -82,10 +77,66 @@ defmodule BorutaIdentityWeb.UserSessionControllerTest do "user" => %{"email" => user.username, "password" => valid_user_password()} }) - assert get_session(conn, :user_token) + assert get_session(conn, session_key(conn)) assert redirected_to(conn) == "/user_return_to" end + test "scope the session by client", %{conn: conn, user: user, request: request} do + conn = + post(conn, Routes.user_session_path(conn, :create, request: request), %{ + "user" => %{"email" => user.username, "password" => valid_user_password()} + }) + + assert get_session(conn, session_key(conn)) + assert redirected_to(conn) == "/user_return_to" + + identity_provider = + BorutaIdentity.Factory.insert(:identity_provider) + + client = Boruta.Factory.insert(:client) + + client_identity_provider = + BorutaIdentity.Factory.insert(:client_identity_provider, + identity_provider: identity_provider, + client_id: client.id + ) + + {:ok, request, _payload} = + Joken.encode_and_sign( + %{ + "client_id" => client_identity_provider.client_id, + "scope" => "", + "user_return_to" => "/user_return_to" + }, + BorutaIdentityWeb.Token.application_signer() + ) + + user = user_fixture(%{backend: identity_provider.backend}) + + conn = + post(conn, Routes.user_session_path(conn, :create, request: request), %{ + "user" => %{"email" => user.username, "password" => valid_user_password()} + }) + + assert get_session(conn, session_key(conn)) + assert redirected_to(conn) == "/user_return_to" + end + + test "logs the user in with remember me", %{conn: conn, user: user, request: request} do + conn = + post(conn, Routes.user_session_path(conn, :create, request: request), %{ + "user" => %{ + "email" => user.username, + "password" => valid_user_password(), + "remember_me" => true + } + }) + + assert get_session(conn, session_key(conn)) + assert redirected_to(conn) == "/user_return_to" + assert conn.resp_cookies["_boruta_identity_web_user_remember_me"] + end + test "logs the user in with totp identity provider", %{ conn: conn, user: user, diff --git a/apps/boruta_identity/test/boruta_identity_web/plugs/sessions_test.exs b/apps/boruta_identity/test/boruta_identity_web/plugs/sessions_test.exs index ed9e6cc82..fcf74a252 100644 --- a/apps/boruta_identity/test/boruta_identity_web/plugs/sessions_test.exs +++ b/apps/boruta_identity/test/boruta_identity_web/plugs/sessions_test.exs @@ -5,7 +5,7 @@ defmodule BorutaIdentityWeb.SessionsTest do alias BorutaIdentityWeb.Sessions - @remember_me_cookie "_boruta_identity_web_user_remember_me" + setup :with_a_request setup %{conn: conn} do conn = @@ -18,9 +18,19 @@ defmodule BorutaIdentityWeb.SessionsTest do end describe "fetch_current_user/2" do - test "authenticates user from session", %{conn: conn, user: user} do + test "authenticates user from session", %{ + conn: conn, + client: client, + user: user, + request: request + } do user_token = generate_user_session_token(user) - conn = conn |> put_session(:user_token, user_token) |> Sessions.fetch_current_user([]) + + conn = + %{conn | query_params: %{"request" => request}} + |> put_session(client.id, user_token) + |> Sessions.fetch_current_user([]) + assert conn.assigns.current_user.id == user.id end @@ -30,24 +40,6 @@ defmodule BorutaIdentityWeb.SessionsTest do refute get_session(conn, :user_token) refute conn.assigns.current_user end - - test "authenticates user from cookies", %{conn: conn, user: user} do - logged_in_conn = - conn - |> fetch_cookies() - |> log_in(user, %{"user" => %{"remember_me" => "true"}}) - - user_token = logged_in_conn.cookies[@remember_me_cookie] - %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie] - - conn = - conn - |> put_req_cookie(@remember_me_cookie, signed_token) - |> Sessions.fetch_current_user([]) - - assert get_session(conn, :user_token) == user_token - assert conn.assigns.current_user.id == user.id - end end describe "redirect_if_user_is_authenticated/2" do diff --git a/apps/boruta_identity/test/support/conn_case.ex b/apps/boruta_identity/test/support/conn_case.ex index 5a84a2e08..2ba137305 100644 --- a/apps/boruta_identity/test/support/conn_case.ex +++ b/apps/boruta_identity/test/support/conn_case.ex @@ -15,6 +15,8 @@ defmodule BorutaIdentityWeb.ConnCase do this option is not recommended for other databases. """ + import BorutaIdentityWeb.Authenticable, only: [session_key: 1] + use ExUnit.CaseTemplate alias BorutaIdentity.Accounts.User @@ -23,6 +25,8 @@ defmodule BorutaIdentityWeb.ConnCase do alias BorutaIdentityWeb.Authenticable alias Ecto.Adapters.SQL.Sandbox + @request_client_id "a2836dfd-c2c7-4b13-a3c3-d408845d2ead" + using do quote do # Import conveniences for testing with connections @@ -80,12 +84,12 @@ defmodule BorutaIdentityWeb.ConnCase do It returns an updated `conn`. """ def log_in(conn, user, params \\ %{}) do - token = generate_user_session_token(user) + session_token = generate_user_session_token(user) conn |> Phoenix.ConnTest.init_test_session(%{}) |> Map.put(:body_params, params) - |> Authenticable.store_user_session(token) + |> Plug.Conn.put_session(@request_client_id, session_token) end def with_a_request(_params) do @@ -98,7 +102,7 @@ defmodule BorutaIdentityWeb.ConnCase do backend: BorutaIdentity.Factory.build(:smtp_backend) ) - client = Boruta.Factory.insert(:client) + client = Boruta.Factory.insert(:client, id: @request_client_id) scope = Boruta.Factory.insert(:scope, name: "request:scope", label: "Scope from request") client_identity_provider =