add oidc tests

This commit is contained in:
Moritz 2025-11-05 18:15:06 +01:00
parent b8afaff2c2
commit 7f5839a120
Signed by: moritz
GPG key ID: 1020A035E5DD0824
3 changed files with 730 additions and 4 deletions

View file

@ -54,10 +54,130 @@ defmodule MvWeb.OidcIntegrationTest do
end
end
describe "OIDC sign-in security tests" do
@tag :test_proposal
test "sign_in_with_rauthy does NOT match user with only email (no oidc_id)" do
# SECURITY TEST: Ensure password-only users cannot be accessed via OIDC
# Create a password-only user (no oidc_id)
_password_user =
create_test_user(%{
email: "password.only@example.com",
password: "securepassword123",
oidc_id: nil
})
# Try to sign in with OIDC using the same email
user_info = %{
"sub" => "attacker_oidc_456",
"preferred_username" => "password.only@example.com"
}
# Should NOT find any 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("Expected no user match, got: #{inspect(other)}")
end
end
@tag :test_proposal
test "sign_in_with_rauthy only matches when oidc_id matches" do
# Create user with specific OIDC ID
user =
create_test_user(%{
email: "oidc.user@example.com",
oidc_id: "correct_oidc_789"
})
# Try with correct oidc_id
correct_user_info = %{
"sub" => "correct_oidc_789",
"preferred_username" => "oidc.user@example.com"
}
{:ok, [found_user]} =
Mv.Accounts.read_sign_in_with_rauthy(%{
user_info: correct_user_info,
oauth_tokens: %{}
})
assert found_user.id == user.id
# Try with wrong oidc_id but correct email
wrong_user_info = %{
"sub" => "wrong_oidc_999",
"preferred_username" => "oidc.user@example.com"
}
result =
Mv.Accounts.read_sign_in_with_rauthy(%{
user_info: wrong_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("Expected no user match when oidc_id differs, got: #{inspect(other)}")
end
end
@tag :test_proposal
test "sign_in_with_rauthy does not match user with empty string oidc_id" do
# Edge case: empty string should be treated like nil
_user =
create_test_user(%{
email: "empty.oidc@example.com",
oidc_id: ""
})
user_info = %{
"sub" => "new_oidc_111",
"preferred_username" => "empty.oidc@example.com"
}
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("Expected no user match with empty oidc_id, got: #{inspect(other)}")
end
end
end
describe "OIDC error and edge case scenarios" do
test "OIDC registration with conflicting email and OIDC ID shows error" do
# Create user with email and OIDC ID
_existing_user =
existing_user =
create_test_user(%{
email: "conflict@example.com",
oidc_id: "oidc_conflict_1"
@ -75,12 +195,15 @@ defmodule MvWeb.OidcIntegrationTest do
oauth_tokens: %{}
})
# Should fail due to unique constraint
# Should fail with PasswordVerificationRequired (account conflict)
# This prevents someone with OIDC provider B from taking over an account
# that's already linked to OIDC provider A
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
# Should contain PasswordVerificationRequired error
assert Enum.any?(errors, fn
%Ash.Error.Changes.InvalidAttribute{field: :email, message: message} ->
String.contains?(message, "has already been taken")
%Mv.Accounts.User.Errors.PasswordVerificationRequired{user_id: user_id} ->
user_id == existing_user.id
_ ->
false