{"record":{"id":"0a0969d4c18d6e8b","repo":"elsa-workflows/elsa-core","slug":"the-requested-elsa-user-is-outside-the-target-tenant","errorCode":null,"errorMessage":"The requested Elsa user is outside the target tenant.","messagePattern":"The requested Elsa user is outside the target tenant\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication/Services/ExternalIdentityUserProvisioningService.cs","lineNumber":35,"sourceCode":"    IIdentityGenerator identityGenerator)\n{\n    private const int MaximumUserNameAttempts = 10;\n\n    /// <summary>\n    /// Resolves an explicitly selected user or creates a credential-less user from the supplied proposal.\n    /// </summary>\n    public async ValueTask<(User User, bool WasCreated)> ResolveAsync(\n        ProvisioningRequest request,\n        Func<string, bool>? tryReserveUserName = null,\n        CancellationToken cancellationToken = default)\n    {\n        if (!string.IsNullOrWhiteSpace(request.ExistingUserId))\n        {\n            var existingUser = await userProvider.FindAsync(new()\n                                   { Id = request.ExistingUserId }, cancellationToken)\n                ?? throw new InvalidOperationException(\"The requested Elsa user does not exist.\");\n            if (!string.Equals(existingUser.TenantId, request.TenantId, StringComparison.Ordinal))\n                throw new InvalidOperationException(\"The requested Elsa user is outside the target tenant.\");\n\n            return (existingUser, false);\n        }\n\n        var proposal = request.Proposal ?? throw new InvalidOperationException(\"A user creation proposal is required for an unlinked external identity.\");\n        var roleIds = await ResolveRoleIdsAsync(proposal.DefaultRoleIds, cancellationToken);\n        var prefix = NormalizeUserNamePrefix(proposal.UserNamePrefix);\n        for (var attempt = 0; attempt < MaximumUserNameAttempts; attempt++)\n        {\n            var name = $\"{prefix}-{identityGenerator.GenerateId()}\";\n            if (tryReserveUserName is not null && !tryReserveUserName(name))\n                continue;\n            if (await userProvider.FindAsync(new()\n                    { Name = name }, cancellationToken) is not null)\n                continue;\n\n            var user = new User\n            {","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication/Services/ExternalIdentityUserProvisioningService.cs#L17-L53","documentation":"ResolveAsync throws this when the resolved existing user's TenantId does not exactly (ordinal, case-sensitive) match request.TenantId. Elsa is multi-tenant: an external identity may only be linked to a user inside the tenant the request targets, so cross-tenant references are rejected rather than silently re-linked.","triggerScenarios":"Calling ResolveAsync with ExistingUserId pointing at a valid user that belongs to a different tenant than request.TenantId — e.g. after enabling multi-tenancy on an app that previously had single-tenant users with null/different TenantId, or an external identity link copied across tenant migrations.","commonSituations":"Enabling multi-tenancy after upgrade so existing users have TenantId null while the request carries a concrete tenant ID; configuring the wrong tenant resolver so requests claim the wrong tenant; moving identity links between tenants during data migration.","solutions":["Ensure request.TenantId matches the target user's TenantId exactly (including case and null vs empty).","If multi-tenancy was recently enabled, migrate legacy users to the correct TenantId instead of linking cross-tenant.","Fix tenant resolution (tenant header/route resolvers) so the provisioning request targets the tenant that owns the user.","Re-create the external identity link within the correct tenant so a new user is provisioned there."],"exampleFix":"// before\nvar request = new ProvisioningRequest { ExistingUserId = userId, TenantId = currentTenantId };\n// after: verify tenant match first\nvar user = await userProvider.FindAsync(new() { Id = userId }, ct);\nif (user is null || !string.Equals(user.TenantId, currentTenantId, StringComparison.Ordinal))\n    request.ExistingUserId = null; // let the proposal path create a user in the right tenant","handlingStrategy":"validation","validationCode":"var user = await userProvider.FindAsync(new() { Id = request.ExistingUserId }, ct);\nif (user is not null && !string.Equals(user.TenantId, request.TenantId, StringComparison.Ordinal))\n    request.ExistingUserId = null; // cross-tenant link: re-provision in the target tenant","typeGuard":"bool IsSameTenant(User? user, string? tenantId) => user is not null && string.Equals(user.TenantId, tenantId, StringComparison.Ordinal);","tryCatchPattern":"try\n{\n    var (user, created) = await provisioning.ResolveAsync(request, ct: ct);\n}\ncatch (InvalidOperationException ex) when (ex.Message == \"The requested Elsa user is outside the target tenant.\")\n{\n    logger.LogWarning(\"Cross-tenant identity link for {UserId}; re-provisioning in {TenantId}\", request.ExistingUserId, request.TenantId);\n    request.ExistingUserId = null;\n    var (user, created) = await provisioning.ResolveAsync(request, ct: ct);\n}","preventionTips":["When enabling multi-tenancy, run a migration that sets TenantId on all existing users.","Test tenant resolution configuration so requests carry the tenant that actually owns the user.","Compare tenant IDs with StringComparison.Ordinal consistently (null vs empty matters)."],"tags":["identity","multi-tenancy","tenant-mismatch","provisioning"],"backgroundTag":"invalid-argument-value","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}