Set the shared account's password before claiming it

Provisioning assigned AuthenticationProviderId and only then called
IUserManager.ChangePassword. Jellyfin dispatches that call to the provider the
user is currently assigned to, so it reached this plugin's own ChangePassword,
which refuses by design. Creating a group by typing "alice+bob" at the login
screen therefore died with NotSupportedException.

Set the placeholder password first, while the freshly created account is still
on Jellyfin's default provider, then claim it.

The new end-to-end tests wire the real provisioning, group and authentication
services together rather than mocking IProvisioningService, and cover a group
created on demand being unlocked afterwards by either member's password. With
the old ordering restored, six of them fail with the original exception.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 10:56:36 +02:00
co-authored by Claude Opus 5
parent 69f7a87cef
commit bb8814644c
3 changed files with 321 additions and 5 deletions
@@ -100,15 +100,20 @@ public class ProvisioningService : IProvisioningService
var sharedUser = await _userManager.CreateUserAsync(accountName).ConfigureAwait(false);
// The shared account never authenticates against its own password - our provider checks
// member hashes instead. Setting a random one avoids leaving a passwordless account behind
// if the provider is ever unassigned.
//
// This must happen before the account is claimed below. IUserManager.ChangePassword
// dispatches to the provider the user is currently assigned to, and ours refuses the call
// by design, so claiming first would make provisioning throw NotSupportedException. A
// freshly created user is still on Jellyfin's default provider, which stores the hash.
await _userManager.ChangePassword(sharedUser, GenerateUnusedPassword()).ConfigureAwait(false);
// Route this account's logins through our provider. Jellyfin matches providers by
// GetType().FullName, the same key the SSO plugin uses, and the assignment only sticks
// once the user is updated.
sharedUser.AuthenticationProviderId = AuthProviderId;
// The shared account never authenticates against its own password - our provider checks
// member hashes instead. Setting a random one avoids leaving a passwordless account behind
// if the provider is ever unassigned.
await _userManager.ChangePassword(sharedUser, GenerateUnusedPassword()).ConfigureAwait(false);
await _userManager.UpdateUserAsync(sharedUser).ConfigureAwait(false);
await ApplyLibraryAccessAsync(sharedUser.Id, distinctIds).ConfigureAwait(false);