Require verified OIDC email for account linking (#963)

Fixes a security issue relying on plain email fields for OIDC user
linking. Requires verified email instead.
This commit is contained in:
Alex
2026-05-09 07:24:31 +01:00
committed by GitHub
parent 9b8402c9a7
commit fbbff8f715
2 changed files with 68 additions and 1 deletions
+57
View File
@@ -445,6 +445,7 @@ class TestOIDCCallbackEndpoint:
"userinfo": {
"sub": "oidc-alice-sub",
"email": "alice@example.com",
"email_verified": True,
"preferred_username": "alice_oidc",
"groups": [],
}
@@ -548,6 +549,7 @@ class TestOIDCCallbackEndpoint:
"userinfo": {
"sub": "oidc-new-sub",
"email": "shared@example.com",
"email_verified": True,
"preferred_username": "oidcuser",
"groups": [],
}
@@ -574,6 +576,7 @@ class TestOIDCCallbackEndpoint:
"userinfo": {
"sub": "oidc-nomatch",
"email": "different@example.com",
"email_verified": True,
"preferred_username": "newuser",
"groups": [],
}
@@ -589,6 +592,60 @@ class TestOIDCCallbackEndpoint:
original = user_db.get_user(username="existing")
assert original["oidc_subject"] is None
@patch("shelfmark.core.oidc_routes._get_oidc_client")
def test_callback_does_not_link_with_unverified_email(self, mock_get_client, client, user_db):
"""OIDC login should not link by email when email_verified is false."""
user_db.create_user(username="existing", email="shared@example.com", password_hash="hash")
fake_client = Mock()
fake_client.authorize_access_token.return_value = {
"userinfo": {
"sub": "oidc-unverified",
"email": "shared@example.com",
"email_verified": False,
"preferred_username": "attackeruser",
"groups": [],
}
}
mock_get_client.return_value = (fake_client, MOCK_OIDC_CONFIG)
resp = client.get("/api/auth/oidc/callback?code=abc123&state=test-state")
assert resp.status_code == 302
with client.session_transaction() as sess:
assert sess["user_id"] == "attackeruser"
original = user_db.get_user(username="existing")
assert original["oidc_subject"] is None
@patch("shelfmark.core.oidc_routes._get_oidc_client")
def test_callback_rejects_unverified_email_link_when_no_provision(
self, mock_get_client, client, user_db
):
"""OIDC login should not link by unverified email when creation is disabled."""
config = {**MOCK_OIDC_CONFIG, "OIDC_AUTO_PROVISION": False}
user_db.create_user(username="existing", email="shared@example.com", password_hash="hash")
fake_client = Mock()
fake_client.authorize_access_token.return_value = {
"userinfo": {
"sub": "oidc-unverified-no-provision",
"email": "shared@example.com",
"email_verified": False,
"preferred_username": "attackeruser",
"groups": [],
}
}
mock_get_client.return_value = (fake_client, config)
resp = client.get("/api/auth/oidc/callback?code=abc123&state=test-state")
error = _get_oidc_error(resp)
assert error is not None
assert "Account not found" in error
original = user_db.get_user(username="existing")
assert original["oidc_subject"] is None
@patch("shelfmark.core.oidc_routes._get_oidc_client")
def test_callback_no_email_link_when_oidc_has_no_email(self, mock_get_client, client, user_db):
"""OIDC login without email in claims should not attempt email linking."""