diff --git a/config/dev.exs b/config/dev.exs index af6e92c..a90ac0d 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -95,3 +95,8 @@ config :mv, :rauthy, base_url: "http://localhost:8080/auth/v1", client_secret: System.get_env("OIDC_CLIENT_SECRET"), redirect_uri: "http://localhost:4000/auth/user/rauthy/callback" + +# AshAuthentication development configuration +config :mv, :session_identifier, nil + +config :mv, :require_token_presence_for_authentication, true diff --git a/config/runtime.exs b/config/runtime.exs index 264ae16..0ce1643 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -55,6 +55,11 @@ if config_env() == :prod do config :mv, :rauthy, redirect_uri: "http://localhost:4000/auth/user/rauthy/callback" + # AshAuthentication production configuration + config :mv, :session_identifier, nil + + config :mv, :require_token_presence_for_authentication, true + config :mv, MvWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ diff --git a/config/test.exs b/config/test.exs index 01a8ae8..bcb55eb 100644 --- a/config/test.exs +++ b/config/test.exs @@ -36,3 +36,12 @@ config :phoenix, :plug_init_mode, :runtime # Enable helpful, but potentially expensive runtime checks config :phoenix_live_view, enable_expensive_runtime_checks: true + +# Token signing secret for AshAuthentication tests +config :mv, :token_signing_secret, "test_secret_key_for_ash_authentication_tokens" + +# AshAuthentication test-specific configuration +# In Tests we don't need token presence, but in other envs its recommended +config :mv, :session_identifier, :unsafe + +config :mv, :require_token_presence_for_authentication, false diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index 8d2946c..15e3a22 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -19,10 +19,17 @@ defmodule Mv.Accounts.User do Currently password and SSO with Rauthy as OIDC provider """ authentication do + session_identifier Application.get_env(:mv, :session_identifier) + tokens do enabled? true token_resource Mv.Accounts.Token - require_token_presence_for_authentication? true + + require_token_presence_for_authentication? Application.get_env( + :mv, + :require_token_presence_for_authentication + ) + store_all_tokens? true # signing_algorithm "EdDSA" -> https://git.local-it.org/local-it/mitgliederverwaltung/issues/87 diff --git a/test/mv_web/controllers/auth_controller_test.exs b/test/mv_web/controllers/auth_controller_test.exs new file mode 100644 index 0000000..e18ade2 --- /dev/null +++ b/test/mv_web/controllers/auth_controller_test.exs @@ -0,0 +1,48 @@ +defmodule MvWeb.AuthControllerTest do + use MvWeb.ConnCase, async: true + + test "GET /sign-in shows sign in form", %{conn: conn} do + conn = get(conn, ~p"/sign-in") + assert html_response(conn, 200) =~ "Sign in" + end + + test "POST /sign-in with valid credentials redirects to home", %{conn: conn} do + # Create a test user first + conn = conn_with_oidc_user(conn) + conn = get(conn, ~p"/sign-in") + + assert redirected_to(conn) == ~p"/" + end + + test "POST /sign-in with invalid credentials shows error", %{conn: conn} do + conn = + post(conn, ~p"/auth/sign_in", %{ + "user" => %{ + "email" => "wrong@example.com", + "password" => "wrongpassword" + } + }) + + assert conn.status == 404 + end + + test "GET /sign-out redirects to home", %{conn: conn} do + # First sign in a user + conn = conn_with_oidc_user(conn) + + # Then sign out + conn = get(conn, ~p"/sign-out") + assert redirected_to(conn) == ~p"/" + end + + test "unauthenticated user accessing protected route gets redirected to sign-in", %{conn: conn} do + conn = get(conn, ~p"/members") + assert redirected_to(conn) == ~p"/sign-in" + end + + test "authenticated user can access protected route", %{conn: conn} do + conn = conn_with_oidc_user(conn) + conn = get(conn, ~p"/members") + assert conn.status == 200 + end +end diff --git a/test/mv_web/controllers/page_controller_test.exs b/test/mv_web/controllers/page_controller_test.exs index 702cd78..ce3195b 100644 --- a/test/mv_web/controllers/page_controller_test.exs +++ b/test/mv_web/controllers/page_controller_test.exs @@ -2,7 +2,9 @@ defmodule MvWeb.PageControllerTest do use MvWeb.ConnCase test "GET /", %{conn: conn} do + conn = conn_with_oidc_user(conn) + conn = get(conn, ~p"/") - assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + assert html_response(conn, 200) =~ "Mitgliederverwaltung" end end diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index b5a5968..ce47a43 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -3,6 +3,7 @@ defmodule MvWeb.MemberLive.IndexTest do import Phoenix.LiveViewTest test "shows translated title in German", %{conn: conn} do + conn = conn_with_oidc_user(conn) conn = Plug.Test.init_test_session(conn, locale: "de") {:ok, _view, html} = live(conn, "/members") # Expected German title @@ -10,6 +11,7 @@ defmodule MvWeb.MemberLive.IndexTest do end test "shows translated title in English", %{conn: conn} do + conn = conn_with_oidc_user(conn) Gettext.put_locale(MvWeb.Gettext, "en") {:ok, _view, html} = live(conn, "/members") # Expected English title @@ -17,18 +19,21 @@ defmodule MvWeb.MemberLive.IndexTest do end test "shows translated button text in German", %{conn: conn} do + conn = conn_with_oidc_user(conn) conn = Plug.Test.init_test_session(conn, locale: "de") {:ok, _view, html} = live(conn, "/members/new") assert html =~ "Speichern" end test "shows translated button text in English", %{conn: conn} do + conn = conn_with_oidc_user(conn) Gettext.put_locale(MvWeb.Gettext, "en") {:ok, _view, html} = live(conn, "/members/new") assert html =~ "Save" end test "shows translated flash message after creating a member in German", %{conn: conn} do + conn = conn_with_oidc_user(conn) conn = Plug.Test.init_test_session(conn, locale: "de") {:ok, view, _html} = live(conn, "/members") view |> element("a", "Neues Mitglied") |> render_click() @@ -44,6 +49,7 @@ defmodule MvWeb.MemberLive.IndexTest do end test "shows translated flash message after creating a member in English", %{conn: conn} do + conn = conn_with_oidc_user(conn) conn = Plug.Test.init_test_session(conn, locale: "en") {:ok, view, _html} = live(conn, "/members") view |> element("a", "New Member") |> render_click() diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 7101531..d1804b7 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -31,6 +31,38 @@ defmodule MvWeb.ConnCase do end end + @doc """ + Creates a test user and returns the user struct. + """ + def create_test_user(attrs \\ %{}) do + email = "user@example.com" + password = "password" + {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password) + + Ash.Seed.seed!(Mv.Accounts.User, %{ + email: email, + hashed_password: hashed_password + }) + end + + @doc """ + Signs in a user via OIDC for testing by creating a session with the user's token. + """ + def sign_in_user_via_oidc(conn, user) do + # Mock OIDC sign-in by creating a token directly + conn + |> Phoenix.ConnTest.init_test_session(%{}) + |> AshAuthentication.Plug.Helpers.store_in_session(user) + end + + @doc """ + Signs in a user via OIDC and returns a connection with the user authenticated. + """ + def conn_with_oidc_user(conn, user_attrs \\ %{}) do + user = create_test_user(user_attrs) + sign_in_user_via_oidc(conn, user) + end + setup tags do Mv.DataCase.setup_sandbox(tags) {:ok, conn: Phoenix.ConnTest.build_conn()}