Skip to content

Refuse authentication service takeover of unrelated accounts - #27380

Open
dennisoelkers wants to merge 3 commits into
masterfrom
fix/auth-service-local-account-takeover
Open

dennisoelkers wants to merge 3 commits into
masterfrom
fix/auth-service-local-account-takeover

Conversation

@dennisoelkers

Copy link
Copy Markdown
Member

Description

Motivation and Context

ProvisionerService looks an existing user up with

`userService.loadByAuthServiceUidOrUsername(uid, username)`

which matches on the authentication service UID or the username, and then updates that account: it sets external to true, overwrites the authentication service id and uid, the name and the email, and keeps the roles and permissions that are already on it.

A username match alone says nothing about the account belonging to the identity being provisioned. An external identity whose username happened to equal a local account therefore inherited that account's roles - up to the Admin role - on its first login, and the original user was locked out because the internal backend refuses password logins for external users. This applies to every backend: LDAP, Active Directory, OIDC, OAuth, SAML and cloud Okta. The built-in root user was already safe because saving it throws, but Mongo-backed administrators were not.

Provisioning now accepts an existing account only when the authentication service UID matches, which is the same identity under a possibly new username, or when the account is already external and managed by this very authentication service, which covers a UID change. Anything else fails with a message naming the conflict.

Deployments that rely on the old behaviour to migrate local accounts onto an authentication service can set the new allow_auth_service_account_takeover option, which defaults to false and logs a warning whenever it lets a takeover through.

Also replaces orElse(createUser(...)) with orElseGet, which was building a throwaway user on every provisioning run even when one was found.

How Has This Been Tested?

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactoring (non-breaking change)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have requested a documentation update.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.

dennisoelkers and others added 3 commits September 14, 2026 10:11
ProvisionerService looks an existing user up with

    userService.loadByAuthServiceUidOrUsername(uid, username)

which matches on the authentication service UID *or* the username, and
then updates that account: it sets external to true, overwrites the
authentication service id and uid, the name and the email, and keeps the
roles and permissions that are already on it.

A username match alone says nothing about the account belonging to the
identity being provisioned. An external identity whose username happened
to equal a local account therefore inherited that account's roles - up to
the Admin role - on its first login, and the original user was locked out
because the internal backend refuses password logins for external users.
This applies to every backend: LDAP, Active Directory, OIDC, OAuth, SAML
and cloud Okta. The built-in root user was already safe because saving it
throws, but Mongo-backed administrators were not.

Provisioning now accepts an existing account only when the authentication
service UID matches, which is the same identity under a possibly new
username, or when the account is already external and managed by this
very authentication service, which covers a UID change. Anything else
fails with a message naming the conflict.

Deployments that rely on the old behaviour to migrate local accounts onto
an authentication service can set the new
allow_auth_service_account_takeover option, which defaults to false and
logs a warning whenever it lets a takeover through.

Also replaces orElse(createUser(...)) with orElseGet, which was building
a throwaway user on every provisioning run even when one was found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant