Open Closed

Shared User Account - invite admin user not using email #10624


User avatar
0
thanhvl1 created

Hi,

I'm trying to use feature shared user account "Inviting an Admin After Tenant Creation" like this guide https://github.com/abpframework/abp/blob/dev/docs/en/modules/account/shared-user-accounts.md

But unfortunately this use email to send invitation, but in my company's network, It doesn't allow to call to smtp or any email server. So how to by pass this in admin UI to accept this invitation or even what tables to update/insert to be able by pass this email flow and simulate user already click accept through email ?

I have tried to duplicated admin user from host and change TenantId of the tenant I use to test but when try to swich tenant in Admin UI, I check log of auth-server I got this

and then I tried to add grant_type SwitchTenant to "Angular" OpenIddict App

Now another error comes

Thanks

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

7 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    A couple of things to clear up first:

    • The "copy host admin and change TenantId" trick won't work — the tenant admin lives in the tenant DB (Isolated mode) and just changing TenantId on a host user breaks the multi-tenant integrity.
    • SwitchTenant grant only works when AbpMultiTenancyOptions.UserSharingStrategy == Shared. In Isolated mode the grant handler explicitly returns unsupported_grant_type, so adding it on the Angular OpenIddict client has no effect — please remove SwitchTenant from the Angular app's ExtensionGrantTypes.

    For your "no SMTP allowed" scenario, the cleanest workaround is to replace the OOTB IIdentityEmailSender so the existing "Invite admin" button never calls SMTP. Instead, the accept-link is written to the log (or your own table / webhook / Redis — whatever your ops team can read). The host admin grabs the link from there and forwards it to the invitee through any internal channel (Slack/Teams/internal portal). The invitee opens the link, the OOTB Account/InviteUser page loads, they pick a username/password and accept.

    Zero UI change, zero new endpoints, no client-proxy regeneration in layered solutions. The OOTB "Invite admin" button keeps working unchanged in MVC, Blazor, and Angular.

    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(IIdentityEmailSender))]
    public class NoSmtpIdentityEmailSender : IdentityEmailSender
    {
        public NoSmtpIdentityEmailSender(
            IEmailSender emailSender,
            ITemplateRenderer templateRenderer,
            IStringLocalizer<IdentityResource> stringLocalizer,
            IAppUrlProvider appUrlProvider,
            UserSharingManager userSharingManager,
            UserInvitationManager userInvitationManager)
            : base(emailSender, templateRenderer, stringLocalizer, appUrlProvider,
                   userSharingManager, userInvitationManager)
        {
        }
    
        public override async Task SendInviteEmailAsync(UserInvitation invitation, bool requireRegister, string appName)
        {
            var url = await AppUrlProvider.GetInviteUserUrlAsync(appName);
            var token = await UserInvitationManager.GenerateTokenFromInvitationIdAsync(invitation.Id);
            var link = $"{url}?token={UrlEncoder.Default.Encode(token)}";
    
            Logger.LogWarning(
                "[Invite] Tenant={tenantId} Email={email} RequireRegister={requireRegister} Link={link}",
                invitation.InviterTenantId, invitation.InviteeEmail, requireRegister, link);
    
            // Or persist to your own table / push to a webhook / write to Redis -
            // whatever your ops team can read from to retrieve the link.
        }
    }
    

    Drop this class into your Application module. Each invite produces a log line like:

    [Invite] Tenant=... Email=admin@mycorp.com RequireRegister=True Link=https://your-app/Account/InviteUser?token=CfDJ8...
    

    Just copy the Link=... value and forward it to the invitee. Done — no SMTP, no SwitchTenant, no host-user copying.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    thanhvl1 created

    thanks for your guide, after implement NoSmtpIdentityEmailSender Now I can get the invite link to accept this.

    But the same thing I got related to SwitchTenant

    this is the log

    in code I have also set all services with

    Configure<AbpMultiTenancyOptions>(options =>
    {
        options.IsEnabled = true;
        options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
    });
    

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Glad the NoSmtpIdentityEmailSender part is working.

    The SwitchTenant error is unrelated to UserSharingStrategy = Shared — that switch is correct and required, but the actual blocker is that the Angular OpenIddict application doesn't have the gt:SwitchTenant permission in the database.

    You probably added SwitchTenant to ExtensionGrantTypes in the OpenIddict admin UI, but the next DbMigrator run wipes it out. Look at OpenIddictDataSeeder.cs in your DbMigrator project — CreateApplicationAsync does this:

    if (!HasSameScopes(client, application))
    {
        client.Permissions = JsonSerializer.Serialize(application.Permissions.Select(q => q.ToString()));
        await _applicationManager.UpdateAsync(client.ToModel());
    }
    

    HasSameScopes actually compares the full Permissions JSON, so any permission you add manually in the UI (like gt:SwitchTenant) makes the comparison fail and the seeder overwrites the whole permission list with what's defined in code.

    Fix: add "SwitchTenant" to the Angular client's grantTypes list in OpenIddictDataSeeder.cs, then re-run DbMigrator:

    //Angular Client
    await CreateApplicationAsync(
        name: "Angular",
        type: OpenIddictConstants.ClientTypes.Public,
        consentType: OpenIddictConstants.ConsentTypes.Implicit,
        displayName: "Angular Client",
        secret: null,
        grantTypes: new List<string> {
            OpenIddictConstants.GrantTypes.AuthorizationCode,
            OpenIddictConstants.GrantTypes.RefreshToken,
            OpenIddictConstants.GrantTypes.Password,
            "LinkLogin",
            "Impersonation",
            "SwitchTenant"   // <-- add this
        },
        scopes: ...,
        ...
    );
    

    After DbMigrator runs, the OpenIddictApplicationPermissions table for the Angular client should contain a row gt:SwitchTenant. Then the Switch tenant action in Angular will succeed.

    You can also add new grant types to an application directly from the OpenIddict admin UI (OpenIddict → Applications → edit → ExtensionGrantTypes). It's persisted to the database immediately, but as shown above it will be wiped on the next DbMigrator run unless you keep OpenIddictDataSeeder.cs in sync. So use the UI for quick experiments, and update the seeder for the permanent fix.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    thanhvl1 created

    Hi,

    I have tried to add SwitchTenant in admin UI and check the db it also has gt:SwitchTenant but the things is it's back to my original question, the auth-server throw error "The specified grant type SwitchTenant is not implemented."

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Confirmed — SwitchTenant is registered with the OpenIddict server but its handler is missing from the ABP token-grant dispatcher, that's why you see "not implemented".

    Please add the following to your AuthServer module's ConfigureServices as a workaround:

    using Volo.Abp.Account.Web.ExtensionGrants;
    using Volo.Abp.OpenIddict.ExtensionGrantTypes;
    
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        // ... existing code ...
    
        Configure<AbpOpenIddictExtensionGrantsOptions>(options =>
        {
            options.Grants[SwitchTenantExtensionGrant.ExtensionGrantName] = new SwitchTenantExtensionGrant();
        });
    }
    

    After this, the SwitchTenant flow will dispatch correctly. We'll ship a permanent fix on our side in the next ABP release.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    thanhvl1 created

    This worked perfectly. Thanks for your support

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Great!

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 09, 2026, 11:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.