265 lines
7.6 KiB
Elixir
265 lines
7.6 KiB
Elixir
defmodule Mv.Accounts.UserAuthenticationTest do
|
|
@moduledoc """
|
|
Tests for user authentication and identification mechanisms.
|
|
|
|
This test suite verifies that:
|
|
- Password login correctly identifies users via email
|
|
- OIDC login correctly identifies users via oidc_id
|
|
- Session identifiers work as expected for both authentication methods
|
|
"""
|
|
use MvWeb.ConnCase, async: true
|
|
require Ash.Query
|
|
|
|
describe "Password authentication user identification" do
|
|
@tag :test_proposal
|
|
test "password login uses email as identifier" do
|
|
# Create a user with password authentication (no oidc_id)
|
|
user =
|
|
create_test_user(%{
|
|
email: "password.user@example.com",
|
|
password: "securepassword123",
|
|
oidc_id: nil
|
|
})
|
|
|
|
# Verify that the user can be found by email
|
|
email_to_find = to_string(user.email)
|
|
|
|
{:ok, users} =
|
|
Mv.Accounts.User
|
|
|> Ash.Query.filter(email == ^email_to_find)
|
|
|> Ash.read()
|
|
|
|
assert length(users) == 1
|
|
found_user = List.first(users)
|
|
assert found_user.id == user.id
|
|
assert to_string(found_user.email) == "password.user@example.com"
|
|
assert is_nil(found_user.oidc_id)
|
|
end
|
|
|
|
@tag :test_proposal
|
|
test "password authentication uses email as identity_field" do
|
|
# Verify the configuration: password strategy should use email as identity_field
|
|
# This test checks the AshAuthentication configuration
|
|
|
|
strategies = AshAuthentication.Info.authentication_strategies(Mv.Accounts.User)
|
|
password_strategy = Enum.find(strategies, fn s -> s.name == :password end)
|
|
|
|
assert password_strategy != nil
|
|
assert password_strategy.identity_field == :email
|
|
end
|
|
|
|
@tag :test_proposal
|
|
test "multiple users can exist with different emails" do
|
|
user1 =
|
|
create_test_user(%{
|
|
email: "user1@example.com",
|
|
password: "password123",
|
|
oidc_id: nil
|
|
})
|
|
|
|
user2 =
|
|
create_test_user(%{
|
|
email: "user2@example.com",
|
|
password: "password456",
|
|
oidc_id: nil
|
|
})
|
|
|
|
assert user1.id != user2.id
|
|
assert to_string(user1.email) != to_string(user2.email)
|
|
end
|
|
|
|
@tag :test_proposal
|
|
test "users with same password but different emails are separate accounts" do
|
|
same_password = "shared_password_123"
|
|
|
|
user1 =
|
|
create_test_user(%{
|
|
email: "alice@example.com",
|
|
password: same_password,
|
|
oidc_id: nil
|
|
})
|
|
|
|
user2 =
|
|
create_test_user(%{
|
|
email: "bob@example.com",
|
|
password: same_password,
|
|
oidc_id: nil
|
|
})
|
|
|
|
# Different users despite same password
|
|
assert user1.id != user2.id
|
|
|
|
# Both passwords should hash to different values (bcrypt uses salt)
|
|
assert user1.hashed_password != user2.hashed_password
|
|
end
|
|
end
|
|
|
|
describe "OIDC authentication user identification" do
|
|
@tag :test_proposal
|
|
test "OIDC login with matching oidc_id finds correct user" do
|
|
# Create user with OIDC authentication
|
|
user =
|
|
create_test_user(%{
|
|
email: "oidc.user@example.com",
|
|
oidc_id: "oidc_identifier_12345"
|
|
})
|
|
|
|
# Simulate OIDC callback
|
|
user_info = %{
|
|
"sub" => "oidc_identifier_12345",
|
|
"preferred_username" => "oidc.user@example.com"
|
|
}
|
|
|
|
# Use sign_in_with_rauthy to find user by oidc_id
|
|
# Note: This test will FAIL until we implement the security fix
|
|
# that changes the filter from email to oidc_id
|
|
result =
|
|
Mv.Accounts.read_sign_in_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{}
|
|
})
|
|
|
|
case result do
|
|
{:ok, [found_user]} ->
|
|
assert found_user.id == user.id
|
|
assert found_user.oidc_id == "oidc_identifier_12345"
|
|
|
|
{:ok, []} ->
|
|
flunk("User should be found by oidc_id")
|
|
|
|
{:error, error} ->
|
|
flunk("Unexpected error: #{inspect(error)}")
|
|
end
|
|
end
|
|
|
|
@tag :test_proposal
|
|
test "OIDC login creates new user when both email and oidc_id are new" do
|
|
# Completely new user from OIDC provider
|
|
user_info = %{
|
|
"sub" => "brand_new_oidc_789",
|
|
"preferred_username" => "newuser@example.com"
|
|
}
|
|
|
|
# Should create via register_with_rauthy
|
|
{:ok, new_user} =
|
|
Mv.Accounts.create_register_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{}
|
|
})
|
|
|
|
assert to_string(new_user.email) == "newuser@example.com"
|
|
assert new_user.oidc_id == "brand_new_oidc_789"
|
|
assert is_nil(new_user.hashed_password)
|
|
end
|
|
|
|
@tag :test_proposal
|
|
test "OIDC user can be uniquely identified by oidc_id" do
|
|
user1 =
|
|
create_test_user(%{
|
|
email: "user1@example.com",
|
|
oidc_id: "oidc_unique_1"
|
|
})
|
|
|
|
user2 =
|
|
create_test_user(%{
|
|
email: "user2@example.com",
|
|
oidc_id: "oidc_unique_2"
|
|
})
|
|
|
|
# Find by oidc_id
|
|
{:ok, users1} =
|
|
Mv.Accounts.User
|
|
|> Ash.Query.filter(oidc_id == "oidc_unique_1")
|
|
|> Ash.read()
|
|
|
|
{:ok, users2} =
|
|
Mv.Accounts.User
|
|
|> Ash.Query.filter(oidc_id == "oidc_unique_2")
|
|
|> Ash.read()
|
|
|
|
assert length(users1) == 1
|
|
assert length(users2) == 1
|
|
assert List.first(users1).id == user1.id
|
|
assert List.first(users2).id == user2.id
|
|
end
|
|
end
|
|
|
|
describe "Mixed authentication scenarios" do
|
|
@tag :test_proposal
|
|
test "user with oidc_id cannot be found by email-only query in sign_in_with_rauthy" do
|
|
# This test verifies the security fix: sign_in_with_rauthy should NOT
|
|
# match users by email, only by oidc_id
|
|
|
|
_user =
|
|
create_test_user(%{
|
|
email: "secure@example.com",
|
|
oidc_id: "secure_oidc_999"
|
|
})
|
|
|
|
# Try to sign in with DIFFERENT oidc_id but SAME email
|
|
user_info = %{
|
|
# Different oidc_id!
|
|
"sub" => "attacker_oidc_888",
|
|
# Same email
|
|
"preferred_username" => "secure@example.com"
|
|
}
|
|
|
|
# Should NOT find the user (security requirement)
|
|
result =
|
|
Mv.Accounts.read_sign_in_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{}
|
|
})
|
|
|
|
# Either returns empty list OR authentication error - both mean "user not found"
|
|
case result do
|
|
{:ok, []} ->
|
|
:ok
|
|
|
|
{:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} ->
|
|
:ok
|
|
|
|
other ->
|
|
flunk("sign_in_with_rauthy should not match by email alone, got: #{inspect(other)}")
|
|
end
|
|
end
|
|
|
|
@tag :test_proposal
|
|
test "password user (oidc_id=nil) is not found by sign_in_with_rauthy" do
|
|
# Create a password-only user
|
|
_user =
|
|
create_test_user(%{
|
|
email: "password.only@example.com",
|
|
password: "securepass123",
|
|
oidc_id: nil
|
|
})
|
|
|
|
# Try OIDC sign-in with this email
|
|
user_info = %{
|
|
"sub" => "new_oidc_777",
|
|
"preferred_username" => "password.only@example.com"
|
|
}
|
|
|
|
# Should NOT find the user because oidc_id is nil
|
|
result =
|
|
Mv.Accounts.read_sign_in_with_rauthy(%{
|
|
user_info: user_info,
|
|
oauth_tokens: %{}
|
|
})
|
|
|
|
# Either returns empty list OR authentication error - both mean "user not found"
|
|
case result do
|
|
{:ok, []} ->
|
|
:ok
|
|
|
|
{:error, %Ash.Error.Forbidden{errors: [%AshAuthentication.Errors.AuthenticationFailed{}]}} ->
|
|
:ok
|
|
|
|
other ->
|
|
flunk(
|
|
"Password-only user should not be found by sign_in_with_rauthy, got: #{inspect(other)}"
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|