All checks were successful
continuous-integration/drone/push Build is passing
## Description of the implemented changes The changes were: - [x] Bugfixing - [x] New Feature - [ ] Breaking Change - [x] Refactoring **OIDC-only mode improvements and UX tweaks (success toasts, unauthenticated redirect).** ## What has been changed? ### OIDC-only mode (new feature) - **Admin settings:** "Only OIDC sign-in" is an immediate toggle at the top of the OIDC section (no save button). Enabling it also turns off "Allow direct registration". When OIDC-only is on, the registration checkbox is disabled and shows a tooltip (DaisyUI `<.tooltip>`). - **Backend:** Password sign-in is forbidden via Ash policy (`OidcOnlyActive` check). Password registration is blocked via validation `OidcOnlyBlocksPasswordRegistration`. New plug `OidcOnlySignInRedirect`: when OIDC-only and OIDC are configured, GET `/sign-in` redirects to the OIDC flow; GET `/auth/user/password/sign_in_with_token` is rejected with redirect + flash. `AuthController.success/4` also rejects password sign-in when OIDC-only. - **Tests:** GlobalSettingsLive (OIDC-only UI), AuthController (redirect and password sign-in rejection), User authentication (register_with_password blocked when OIDC-only). ### UX / behaviour (no new feature flag) - **Success toasts:** Success flash messages auto-dismiss after 5 seconds via JS hook `FlashAutoDismiss` and optional `auto_clear_ms` on `<.flash>` (used for success in root layout and `flash_group`). - **Unauthenticated users:** Redirect to sign-in without the "You don't have permission to access this page" flash; that message is only shown to logged-in users who lack access. Logic in `LiveHelpers` and `CheckPagePermission` plug; test updated accordingly. ### Other - Layouts: comment about unprocessed join-request count no longer uses "TODO" (Credo). - Gettext: German translation for "Home" (Startseite); POT/PO kept in sync. - CHANGELOG: Unreleased section updated with the above. ## Definition of Done ### Code Quality - [x] No new technical depths - [x] Linting passed - [x] Documentation is added where needed (module docs, comments where non-obvious) ### Accessibility - [x] New elements are properly defined with html-tags (labels, aria-label on checkboxes) - [x] Colour contrast follows WCAG criteria (unchanged) - [x] Aria labels are added when needed (e.g. oidc-only and registration checkboxes) - [x] Everything is accessible by keyboard (toggles and buttons unchanged) - [x] Tab-Order is comprehensible - [x] All interactive elements have a visible focus (existing patterns) ### Testing - [x] Tests for new code are written (OIDC-only UI, auth controller, user auth; SMTP config builder and mailer) - [x] All tests pass - [ ] axe-core dev tools show no critical or major issues (not re-run for this PR; suggest spot-check on settings and sign-in) ## Additional Notes - **OIDC-only:** When the `OIDC_ONLY` env var is set, the toggle is read-only and shows "(From OIDC_ONLY)". When OIDC is not configured, the toggle is disabled. - **Invalidation:** Enabling OIDC-only sets `registration_enabled: false` in one update; disabling OIDC-only only updates `oidc_only` (registration left as-is). - **Review focus:** Plug order in router (OidcOnlySignInRedirect), policy/validation order in User, and that all OIDC-only paths (form, plug, controller) stay consistent. Reviewed-on: #474 Co-authored-by: Simon <s.thiessen@local-it.org> Co-committed-by: Simon <s.thiessen@local-it.org>
575 lines
18 KiB
Elixir
575 lines
18 KiB
Elixir
defmodule MvWeb.AuthControllerTest do
|
|
use MvWeb.ConnCase, async: true
|
|
import Phoenix.LiveViewTest
|
|
import Phoenix.ConnTest
|
|
import ExUnit.CaptureLog
|
|
|
|
alias Mv.Membership
|
|
|
|
# Helper to create an unauthenticated conn (preserves sandbox metadata)
|
|
defp build_unauthenticated_conn(authenticated_conn) do
|
|
# Create new conn but preserve sandbox metadata for database access
|
|
new_conn =
|
|
build_conn()
|
|
|> init_test_session(%{})
|
|
|> fetch_flash()
|
|
|
|
# Copy sandbox metadata from authenticated conn
|
|
if authenticated_conn.private[:ecto_sandbox] do
|
|
Plug.Conn.put_private(new_conn, :ecto_sandbox, authenticated_conn.private[:ecto_sandbox])
|
|
else
|
|
new_conn
|
|
end
|
|
end
|
|
|
|
# Basic UI tests
|
|
test "GET /sign-in shows sign in form", %{conn: authenticated_conn} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
conn = get(conn, ~p"/sign-in")
|
|
assert html_response(conn, 200) =~ "Sign in"
|
|
end
|
|
|
|
@tag role: :unauthenticated
|
|
test "GET /sign-in returns 200 and renders page (exercises AuthOverrides and layout)", %{
|
|
conn: conn
|
|
} do
|
|
{:ok, _view, html} = live(conn, ~p"/sign-in")
|
|
assert html =~ "Sign in"
|
|
# Public header (logo) from Layouts.app unauthenticated branch
|
|
assert html =~ "mila.svg" or html =~ "Mila Logo"
|
|
end
|
|
|
|
test "GET /sign-out redirects to home", %{conn: authenticated_conn} do
|
|
conn = conn_with_oidc_user(authenticated_conn)
|
|
conn = get(conn, ~p"/sign-out")
|
|
assert redirected_to(conn) == ~p"/"
|
|
end
|
|
|
|
# Password authentication (LiveView)
|
|
test "password user can sign in with valid credentials via LiveView", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
_user =
|
|
create_test_user(%{
|
|
email: "password@example.com",
|
|
password: "secret123",
|
|
oidc_id: nil
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, "/sign-in")
|
|
|
|
{:error, {:redirect, %{to: to}}} =
|
|
view
|
|
|> form("#user-password-sign-in-with-password",
|
|
user: %{email: "password@example.com", password: "secret123"}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert to =~ "/auth/user/password/sign_in_with_token"
|
|
end
|
|
|
|
test "password user with invalid credentials shows error via LiveView", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
_user =
|
|
create_test_user(%{
|
|
email: "test@example.com",
|
|
password: "correct_password",
|
|
oidc_id: nil
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, "/sign-in")
|
|
|
|
html =
|
|
view
|
|
|> form("#user-password-sign-in-with-password",
|
|
user: %{email: "test@example.com", password: "wrong_password"}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert html =~ "Email or password was incorrect"
|
|
end
|
|
|
|
test "password user with non-existent email shows error via LiveView", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
{:ok, view, _html} = live(conn, "/sign-in")
|
|
|
|
html =
|
|
view
|
|
|> form("#user-password-sign-in-with-password",
|
|
user: %{email: "nonexistent@example.com", password: "anypassword"}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert html =~ "Email or password was incorrect"
|
|
end
|
|
|
|
# Registration (LiveView)
|
|
test "user can register with valid credentials via LiveView", %{conn: authenticated_conn} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
{:ok, view, _html} = live(conn, "/register")
|
|
|
|
{:error, {:redirect, %{to: to}}} =
|
|
view
|
|
|> form("#user-password-register-with-password-wrapper form",
|
|
user: %{email: "newuser@example.com", password: "newpassword123"}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert to =~ "/auth/user/password/sign_in_with_token"
|
|
end
|
|
|
|
test "registration with existing email shows error via LiveView", %{conn: authenticated_conn} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
_user =
|
|
create_test_user(%{
|
|
email: "existing@example.com",
|
|
password: "secret123",
|
|
oidc_id: nil
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, "/register")
|
|
|
|
html =
|
|
view
|
|
|> form("#user-password-register-with-password-wrapper form",
|
|
user: %{email: "existing@example.com", password: "anotherpassword"}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert html =~ "has already been taken"
|
|
end
|
|
|
|
test "registration with weak password shows error via LiveView", %{conn: authenticated_conn} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
{:ok, view, _html} = live(conn, "/register")
|
|
|
|
html =
|
|
view
|
|
|> form("#user-password-register-with-password-wrapper form",
|
|
user: %{email: "weakpass@example.com", password: "123"}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert html =~ "length must be greater than or equal to 8"
|
|
end
|
|
|
|
test "when registration is disabled, sign-in page does not show Need an account? toggle", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
{:ok, settings} = Membership.get_settings()
|
|
original = Map.get(settings, :registration_enabled, true)
|
|
{:ok, _} = Membership.update_settings(settings, %{registration_enabled: false})
|
|
|
|
try do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
{:ok, _view, html} = live(conn, ~p"/sign-in")
|
|
refute html =~ "Need an account?"
|
|
after
|
|
{:ok, s} = Membership.get_settings()
|
|
Membership.update_settings(s, %{registration_enabled: original})
|
|
end
|
|
end
|
|
|
|
# Access control
|
|
test "unauthenticated user accessing protected route gets redirected to sign-in", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
conn = get(conn, ~p"/members")
|
|
assert redirected_to(conn) == ~p"/sign-in"
|
|
end
|
|
|
|
test "authenticated user can access protected route", %{conn: authenticated_conn} do
|
|
conn = conn_with_oidc_user(authenticated_conn)
|
|
conn = get(conn, ~p"/members")
|
|
assert conn.status == 200
|
|
end
|
|
|
|
test "password authenticated user can access protected route via LiveView", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
_user =
|
|
create_test_user(%{
|
|
email: "auth@example.com",
|
|
password: "secret123",
|
|
oidc_id: nil
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, "/sign-in")
|
|
|
|
{:error, {:redirect, %{to: to}}} =
|
|
view
|
|
|> form("#user-password-sign-in-with-password",
|
|
user: %{email: "auth@example.com", password: "secret123"}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert to =~ "/auth/user/password/sign_in_with_token"
|
|
|
|
# After login, user is redirected to /auth/user/password/sign_in_with_token.
|
|
# Session handling for protected routes should be tested in integration or E2E tests.
|
|
end
|
|
|
|
# Edge cases
|
|
test "user with nil oidc_id can still sign in with password via LiveView", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
_user =
|
|
create_test_user(%{
|
|
email: "nil_oidc@example.com",
|
|
password: "secret123",
|
|
oidc_id: nil
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, "/sign-in")
|
|
|
|
{:error, {:redirect, %{to: to}}} =
|
|
view
|
|
|> form("#user-password-sign-in-with-password",
|
|
user: %{email: "nil_oidc@example.com", password: "secret123"}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert to =~ "/auth/user/password/sign_in_with_token"
|
|
end
|
|
|
|
test "user with empty string oidc_id is handled correctly via LiveView", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
# Create unauthenticated conn for this test
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
_user =
|
|
create_test_user(%{
|
|
email: "empty_oidc@example.com",
|
|
password: "secret123",
|
|
oidc_id: ""
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, "/sign-in")
|
|
|
|
{:error, {:redirect, %{to: to}}} =
|
|
view
|
|
|> form("#user-password-sign-in-with-password",
|
|
user: %{email: "empty_oidc@example.com", password: "secret123"}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert to =~ "/auth/user/password/sign_in_with_token"
|
|
end
|
|
|
|
describe "when OIDC-only is enabled" do
|
|
setup %{conn: authenticated_conn} do
|
|
{:ok, settings} = Membership.get_settings()
|
|
original_oidc_only = Map.get(settings, :oidc_only, false)
|
|
{:ok, _} = Membership.update_settings(settings, %{oidc_only: true})
|
|
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
{:ok, conn: conn, original_oidc_only: original_oidc_only}
|
|
end
|
|
|
|
test "password sign-in is rejected and redirects to sign-in with error", %{
|
|
conn: conn,
|
|
original_oidc_only: original
|
|
} do
|
|
try do
|
|
_user =
|
|
create_test_user(%{
|
|
email: "password@example.com",
|
|
password: "secret123",
|
|
oidc_id: nil
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, "/sign-in")
|
|
|
|
result =
|
|
view
|
|
|> form("#user-password-sign-in-with-password",
|
|
user: %{email: "password@example.com", password: "secret123"}
|
|
)
|
|
|> render_submit()
|
|
|
|
# When OIDC-only is enabled, password sign-in must not succeed (no redirect to sign_in_with_token).
|
|
case result do
|
|
{:error, {:redirect, %{to: to}}} ->
|
|
refute to =~ "sign_in_with_token",
|
|
"Expected password sign-in to be rejected when OIDC-only, got redirect to: #{to}"
|
|
|
|
_ ->
|
|
# LiveView re-rendered (e.g. with flash error) instead of redirecting to success
|
|
:ok
|
|
end
|
|
after
|
|
{:ok, s} = Membership.get_settings()
|
|
Membership.update_settings(s, %{oidc_only: original})
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "GET /sign-in when OIDC-only" do
|
|
test "redirects to OIDC flow when OIDC-only and OIDC are configured", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
{:ok, settings} = Membership.get_settings()
|
|
|
|
prev = %{
|
|
oidc_only: settings.oidc_only,
|
|
oidc_client_id: settings.oidc_client_id,
|
|
oidc_base_url: settings.oidc_base_url,
|
|
oidc_redirect_uri: settings.oidc_redirect_uri
|
|
}
|
|
|
|
{:ok, _} =
|
|
Membership.update_settings(settings, %{
|
|
oidc_only: true,
|
|
oidc_client_id: "test-client",
|
|
oidc_base_url: "https://idp.example.com",
|
|
oidc_redirect_uri: "http://localhost:4000/auth/user/oidc/callback",
|
|
oidc_client_secret: "test-secret"
|
|
})
|
|
|
|
try do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
conn = get(conn, ~p"/sign-in")
|
|
assert redirected_to(conn) =~ "/auth/user/oidc"
|
|
after
|
|
{:ok, s} = Membership.get_settings()
|
|
Membership.update_settings(s, prev)
|
|
end
|
|
end
|
|
|
|
test "returns 200 when OIDC-only but oidc_failed=1 (avoids redirect loop)", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
{:ok, settings} = Membership.get_settings()
|
|
|
|
prev = %{
|
|
oidc_only: settings.oidc_only,
|
|
oidc_client_id: settings.oidc_client_id,
|
|
oidc_base_url: settings.oidc_base_url,
|
|
oidc_redirect_uri: settings.oidc_redirect_uri
|
|
}
|
|
|
|
{:ok, _} =
|
|
Membership.update_settings(settings, %{
|
|
oidc_only: true,
|
|
oidc_client_id: "test-client",
|
|
oidc_base_url: "https://idp.example.com",
|
|
oidc_redirect_uri: "http://localhost:4000/auth/user/oidc/callback",
|
|
oidc_client_secret: "test-secret"
|
|
})
|
|
|
|
try do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
conn = get(conn, "/sign-in?oidc_failed=1")
|
|
assert conn.status == 200
|
|
# Sign-in page is shown, not redirect to OIDC
|
|
assert conn.resp_body =~ "Sign in" or conn.resp_body =~ "sign-in"
|
|
after
|
|
{:ok, s} = Membership.get_settings()
|
|
Membership.update_settings(s, prev)
|
|
end
|
|
end
|
|
|
|
test "returns 200 when OIDC-only but OIDC not configured", %{conn: authenticated_conn} do
|
|
{:ok, settings} = Membership.get_settings()
|
|
original_oidc_only = Map.get(settings, :oidc_only, false)
|
|
{:ok, _} = Membership.update_settings(settings, %{oidc_only: true})
|
|
|
|
try do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
conn = get(conn, ~p"/sign-in")
|
|
assert conn.status == 200
|
|
after
|
|
{:ok, s} = Membership.get_settings()
|
|
Membership.update_settings(s, %{oidc_only: original_oidc_only})
|
|
end
|
|
end
|
|
|
|
test "returns 200 when OIDC-only is disabled", %{conn: authenticated_conn} do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
conn = get(conn, ~p"/sign-in")
|
|
assert conn.status == 200
|
|
end
|
|
end
|
|
|
|
# OIDC/Rauthy error handling tests
|
|
describe "handle_oidc_failure/2" do
|
|
test "Assent.ServerUnreachableError redirects to sign-in with error flash", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
# Create a mock Assent.ServerUnreachableError struct with required fields
|
|
error = %Assent.ServerUnreachableError{
|
|
http_adapter: Assent.HTTPAdapter.Finch,
|
|
request_url: "https://auth.example.com/callback?token=secret123",
|
|
reason: %Mint.TransportError{reason: :econnrefused}
|
|
}
|
|
|
|
conn = MvWeb.AuthController.failure(conn, {:oidc, :callback}, error)
|
|
|
|
assert redirected_to(conn) == "/sign-in?oidc_failed=1"
|
|
|
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
|
|
"The authentication server is currently unavailable. Please try again later."
|
|
end
|
|
|
|
test "Assent.InvalidResponseError redirects to sign-in with error flash", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
# Create a mock Assent.InvalidResponseError struct with required field
|
|
# InvalidResponseError only has :response field (HTTPResponse struct)
|
|
error = %Assent.InvalidResponseError{
|
|
response: %Assent.HTTPAdapter.HTTPResponse{
|
|
status: 400,
|
|
headers: [],
|
|
body: "invalid_request"
|
|
}
|
|
}
|
|
|
|
conn = MvWeb.AuthController.failure(conn, {:oidc, :callback}, error)
|
|
|
|
assert redirected_to(conn) == "/sign-in?oidc_failed=1"
|
|
|
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
|
|
"Authentication configuration error. Please contact the administrator."
|
|
end
|
|
|
|
test "unknown reason triggers catch-all and redirects to sign-in with error flash", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
unknown_reason = :oops
|
|
|
|
conn = MvWeb.AuthController.failure(conn, {:oidc, :callback}, unknown_reason)
|
|
|
|
assert redirected_to(conn) == "/sign-in?oidc_failed=1"
|
|
|
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
|
|
"Unable to authenticate with OIDC. Please try again."
|
|
end
|
|
end
|
|
|
|
# Logging security tests - ensure no sensitive data is logged
|
|
describe "failure/3 logging security" do
|
|
test "does not log full URL with query params for Assent.ServerUnreachableError", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
error = %Assent.ServerUnreachableError{
|
|
http_adapter: Assent.HTTPAdapter.Finch,
|
|
request_url: "https://auth.example.com/callback?token=secret123&code=abc456",
|
|
reason: %Mint.TransportError{reason: :econnrefused}
|
|
}
|
|
|
|
log =
|
|
capture_log(fn ->
|
|
MvWeb.AuthController.failure(conn, {:oidc, :callback}, error)
|
|
end)
|
|
|
|
# Should log redacted URL (only scheme and host)
|
|
assert log =~ "https://auth.example.com"
|
|
# Should NOT log query parameters or tokens
|
|
refute log =~ "token=secret123"
|
|
refute log =~ "code=abc456"
|
|
refute log =~ "callback?token"
|
|
end
|
|
|
|
test "does not log sensitive data for Assent.InvalidResponseError", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
error = %Assent.InvalidResponseError{
|
|
response: %Assent.HTTPAdapter.HTTPResponse{
|
|
status: 400,
|
|
headers: [],
|
|
body: "invalid_request"
|
|
}
|
|
}
|
|
|
|
log =
|
|
capture_log(fn ->
|
|
MvWeb.AuthController.failure(conn, {:oidc, :callback}, error)
|
|
end)
|
|
|
|
# Should log error type but not full error details
|
|
assert log =~ "Authentication failure"
|
|
assert log =~ "oidc"
|
|
# Should not log full error struct with inspect
|
|
refute log =~ "Assent.InvalidResponseError"
|
|
end
|
|
|
|
test "does not log full reason for unknown OIDC errors", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
# Simulate an error that might contain sensitive data
|
|
error_with_sensitive_data = %{
|
|
token: "secret_token_123",
|
|
url: "https://example.com/callback?access_token=abc123",
|
|
error: :something_went_wrong
|
|
}
|
|
|
|
log =
|
|
capture_log(fn ->
|
|
MvWeb.AuthController.failure(conn, {:oidc, :callback}, error_with_sensitive_data)
|
|
end)
|
|
|
|
# Should log error type but not full error details
|
|
assert log =~ "Authentication failure"
|
|
assert log =~ "oidc"
|
|
# Should NOT log sensitive data
|
|
refute log =~ "secret_token_123"
|
|
refute log =~ "access_token=abc123"
|
|
refute log =~ "callback?access_token"
|
|
end
|
|
|
|
test "logs full reason for non-OIDC activities (password auth)", %{
|
|
conn: authenticated_conn
|
|
} do
|
|
conn = build_unauthenticated_conn(authenticated_conn)
|
|
|
|
reason = %AshAuthentication.Errors.AuthenticationFailed{
|
|
caused_by: %Ash.Error.Forbidden{errors: []}
|
|
}
|
|
|
|
log =
|
|
capture_log(fn ->
|
|
MvWeb.AuthController.failure(conn, {:password, :sign_in}, reason)
|
|
end)
|
|
|
|
# For non-OIDC activities, full reason is safe to log
|
|
assert log =~ "Authentication failure"
|
|
assert log =~ "password"
|
|
assert log =~ "AuthenticationFailed"
|
|
end
|
|
end
|
|
end
|