Activities of "Sturla"

After thinking this over I have decided to go with mulit-tenancy where the user never knows anything about its tenant name. So a unique e-mail accross the SaaS, an email can only be used once!

Context and resoning

  1. The question in the Conclusion of Alper's article helped but also future security/GDPR and future moving of a tenant is a part of it.
  2. Logged in users (email) will be unique so there will only be one email for one tenant! Its not possible to create many tenants with the same email!
  3. Tenant name will be auto generated at registration! The user (a tenant) will never need to know about its tenant name (there will never be a subdomain or custom domain for a tenant) so the tenant name is irrelevant.

| Step | URL | What the user types | What happens internally | | --------------- | ------------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Register | https://yoursite.is/register | E-mail + Password | • Host context creates a new tenant with an auto-generated name (GUID (or first part of the e-mail)). <br>• Creates the first user in that tenant and assigns the TenantAdmin role. | | Login | https://yoursite.is/login | E-mail + Password | A custom LoginModel locates the user across all tenants, switches CurrentTenant to the user’s TenantId, then performs normal sign-in. | | After login | /app | – | CurrentUserTenantResolveContributor keeps the tenant id in the auth cookie, so every request is already scoped correctly. |

Highlevel implementation

  1. Hide ABP’s tenant switcher entirely (no tenant selector)
  2. Self-registration service that creates the tenant name (probably a Guid instead of the first part of email)
  3. Enforce global e-mail uniqueness once at sign-up

I think this is a good conclusion. What do you think?

p.s <br> in the table works when you Preview it but not when published so this is a bug!

Are you sure you want to do this?

No not really 🤦‍♂️

I´m trying to figure out if multi-tenancy is the correct way for my SaaS or not.

What I do know is that I do not want the user to have to input all three

  • user name (or email)
  • password
  • and a tenant name

So maybe this isn´t a multi-tenant system at all... I´m really starting to lean towards that...

What is missing in my question is the following:

  • I want the user input to become the tenant

So when a user registers with username/email/password the username becomes the tenant autmatically. When they then login with a username/password that username is connected to the registered tenant.

Context for wanting this to be multi tenant

  • I´m looking for data separation and security
  • There will never be any custom domain url

I want to do the following

  1. Disable that what ever user can either login or register as a user. There needs to be a tenant created. Is this possible with appsettings.json?
  2. So the login (and registration) page should only have input for Tenant name and password.
  3. Since I have a custom page that point to the login page I need the url to this page (if its not just the Login.cshtml page customized)
  4. Show me what pages I need to customize/create and what I need to configure.

I´m on Blazor Web App (server/client)

Ok I´m going to answer this and suggest that this will be added as whole to to the abp.io documentation (you can fix it if its incurrect but it works now fine for me!)

How to change the color pallet

I didn´t really need to create a new template, I just needed to change the default color pallet schema so it works with every default theme.

Step 1

Create this css file Blazor/wwwroot/css/custom-leptonx-overrides.cs

Step 2

Add the following overrides to it (you choose the colors)

/*
 * YourSite Custom LeptonX Theme Overrides
 * Following ABP LeptonX theme customization guidelines
 * High specificity overrides to ensure they load after LeptonX defaults
 */

/* LeptonX and Bootstrap color variable overrides */
:root {
    /* LeptonX variables */
    --lpx-primary: #BB977D !important;
    --lpx-primary-rgb: 72, 81, 88 !important;
    --lpx-secondary: [#485158](https://abp.io/QA/Questions/485158) !important;
    --lpx-secondary-rgb: 187, 151, 125 !important;
    --lpx-success: #EEA646 !important;
    --lpx-success-rgb: 238, 166, 70 !important;
    --lpx-warning: #EEA646 !important;
    --lpx-warning-rgb: 238, 166, 70 !important;
    --lpx-info: #BB977D !important;
    --lpx-info-rgb: 72, 81, 88 !important;
    --lpx-danger: #ef4444 !important;
    --lpx-danger-rgb: 239, 68, 68 !important;
    --lpx-brand: #BB977D !important;
    --lpx-brand-rgb: 72, 81, 88 !important;
    --lpx-brand-contrast: #fff !important;
    --lpx-brand-hover: #3a434a !important;
    --lpx-brand-active: #23272b !important;
    /* Bootstrap variables */
    --bs-primary: #BB977D !important;
    --bs-primary-rgb: 72, 81, 88 !important;
    --bs-secondary: [#485158](https://abp.io/QA/Questions/485158) !important;
    --bs-secondary-rgb: 187, 151, 125 !important;
    --bs-success: #EEA646 !important;
    --bs-success-rgb: 238, 166, 70 !important;
    --bs-warning: #EEA646 !important;
    --bs-warning-rgb: 238, 166, 70 !important;
    --bs-info: #BB977D !important;
    --bs-info-rgb: 72, 81, 88 !important;
    --bs-danger: #ef4444 !important;
    --bs-danger-rgb: 239, 68, 68 !important;
    /* Button backgrounds and borders */
    --bs-btn-primary-bg: #BB977D !important;
    --bs-btn-primary-border-color: #BB977D !important;
    --bs-btn-primary-hover-bg: #3a434a !important;
    --bs-btn-primary-hover-border-color: #3a434a !important;
    --bs-btn-primary-active-bg: #23272b !important;
    --bs-btn-primary-active-border-color: #23272b !important;
    --bs-btn-primary-color: #fff !important;
    --bs-btn-secondary-bg: [#485158](https://abp.io/QA/Questions/485158) !important;
    --bs-btn-secondary-border-color: [#485158](https://abp.io/QA/Questions/485158) !important;
    --bs-btn-secondary-hover-bg: #a07d5f !important;
    --bs-btn-secondary-hover-border-color: #a07d5f !important;
    --bs-btn-secondary-active-bg: #8a6a4d !important;
    --bs-btn-secondary-active-border-color: #8a6a4d !important;
    --bs-btn-secondary-color: #fff !important;
}

/* High specificity for buttons and LeptonX/Bootstrap classes */
.btn-primary, .lpx-btn-primary {
    background-color: var(--bs-primary) !important;
    border-color: var(--bs-primary) !important;
    color: var(--bs-btn-primary-color) !important;
}
.btn-secondary, .lpx-btn-secondary {
    background-color: var(--bs-secondary) !important;
    border-color: var(--bs-secondary) !important;
    color: var(--bs-btn-secondary-color) !important;
}

/* Optionally, override icon and menu item colors */
.lpx-menu-item.selected .lpx-menu-item-icon,
.lpx-menu-item.selected .lpx-menu-item-link,
.lpx-menu-item.selected .lpx-menu-item-link .lpx-menu-item-icon {
    color: var(--lpx-brand) !important;
}

/* Dark mode overrides (optional, extend as needed). 
   This can be done for all the other themes "light","system" etc. */
[data-bs-theme="dark"], [data-theme="dark"] {
    --lpx-primary: #23272b !important;
    --lpx-secondary: #8a6a4d !important;
    --bs-primary: #23272b !important;
    --bs-secondary: #8a6a4d !important;
}

Step 3: Update main.css in Blazor.Client

Add the css above to the www/main.css file.

Step 4: Create a bundle contriputor

Create this class in Blazor root

public class YourSiteThemeBundleContributor : BundleContributor
{
    public override void ConfigureBundle(BundleConfigurationContext context)
    {
        // Add global styles and custom CSS files in the correct order
        context.Files.Add(new BundleFile("/global-styles.css", true));
        context.Files.Add(new BundleFile("/css/custom-leptonx-overrides.css", true));
        context.Files.Add(new BundleFile("/main.css", true));
    }
}

and update the code in ConfigureBundles() in your module class

Configure<AbpBundlingOptions>(options =>
{
    // Blazor Web App
    options.Parameters.InteractiveAuto = true;


    // MVC UI - Use bundle contributor for global styles
    options.StyleBundles.Configure(
        LeptonXThemeBundles.Styles.Global,
        bundle =>
        {
            bundle.AddContributors(typeof(YourSiteThemeBundleContributor));
        }
    );

    options.ScriptBundles.Configure(
        LeptonXThemeBundles.Scripts.Global,
        bundle =>
        {
            bundle.AddFiles("/global-scripts.js");
        }
    );

    // Blazor UI - Use bundle contributor for global styles
    options.StyleBundles.Configure(
        BlazorLeptonXThemeBundles.Styles.Global,
        bundle =>
        {
            bundle.AddContributors(typeof(YourSiteThemeBundleContributor));
        }
    );
});

I hope this will help the next poor soul out there!

I need information on what steps are needed to create a new LeptonX Theme for Blazor Hybrid app.

  • What files need to be created?
  • What files need to be changed?
    • How to change colors (Primary, Secondary etc.)
  • What configuration in code needs to be done?
  • Other vital informaiton I need to have a new theme.

Secondary questions

  • What do I need to do to just change the color schema of the current LeptonX theme?
    • Primary, Secondary etc. colors
    • The background color (If I like the background in Dark mode to be Brown instead of black)

I´m hoping the AI can answer these super simple questions that I feel the documentation does not answer at all!

Thank you liming but I figured this out finally. It was _Host.cshtml from Elsa that was causing my issue

I hade builder.MapFallbackToPage("/_Host");

and then had to do things arround that.

I have been battling with this issue for two days now and I need assitance.

Observed Problem

  • Browser to my site in ingognito
  • First page load → OK
  • Click “Sign in” (public login page)
  • Submitting credentials triggers HTTP 400 and ~/Views/Error/404 or 400.cshtml.
  • Console log excerpt: Antiforgery token validation failed. Validation of the provided antiforgery token failed. The cookie token and the request token were swapped. Identity.Application was not authenticated. Failure message: Unprotect ticket failed

Environment

| Item | Details | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | ABP version | 9.2.2 (Commercial) | | Template | Blazor Hybrid (Account Public + Admin Pro + LeptonX) | | Elsa Studio | v3 – integrated as standalone WASM under /elsa/… | | Hosting | Azure Container Apps (ACA) – single container, Linux | | Ingress | external=true, targetPort=8080, transport=auto | | Probes | /health (liveness) & /health/ready (readiness/startup) | | Data Protection | Keys persisted to Azure Blob Storage via Azure.Extensions.AspNetCore.DataProtection.Blobs, user‑assigned managed identity | | Forwarded Headers | Middleware added first: app.UseForwardedHeaders(); options include XForwardedProto, XForwardedFor, XForwardedHost | | Antiforgery | HeaderName = "X-XSRF-TOKEN", Cookie.Name = "XSRF-TOKEN", SecurePolicy = Always, SameSite = None | | DataProtection code | .SetApplicationName("YourSite") + .PersistKeysToAzureBlobStorage(blobClient) | | Cookies | Application cookie unchanged; antiforgery cookie now present |

HttpOverrides trace shows Scheme=https and correct host!

Relevant Code Snippets

This is the code I have in my BlazorModule.cs file

ConfigureServices()

// 1️⃣ Forwarded‑Header options (ACA IPs change frequently)
services.Configure<ForwardedHeadersOptions>(o =>
{
    o.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
                         ForwardedHeaders.XForwardedProto |
                         ForwardedHeaders.XForwardedHost;
    o.KnownNetworks.Clear();  // allow any ingress IP
    o.KnownProxies.Clear();
    o.ForwardLimit = null;    // unlimited hops
});

// 2️⃣ Data‑Protection keys → Azure Blob
var blobClient = new BlobClient(new Uri(cfg["DataProtection:BlobUri"]),
                                new DefaultAzureCredential(new DefaultAzureCredentialOptions {
                                    ManagedIdentityClientId = cfg["DataProtection:ManagedIdentityClientId"]
                                }));
services.AddDataProtection()
        .SetApplicationName("YourSite")
        .PersistKeysToAzureBlobStorage(blobClient);

// 3️⃣ Antiforgery settings
services.AddAntiforgery(o =>
{
    o.HeaderName = "X-XSRF-TOKEN";
    o.Cookie.Name = "XSRF-TOKEN";
    o.Cookie.SameSite = SameSiteMode.None;
    o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

// 4️⃣ Pin discriminator after all modules
services.PostConfigure<DataProtectionOptions>(o =>
{
    o.ApplicationDiscriminator = "YourSite";
});

OnApplicationInitialization (first lines)

var app = context.GetApplicationBuilder();

// 1️⃣ MUST be first – handle X‑Forwarded‑* from ACA
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor |
                       ForwardedHeaders.XForwardedProto |
                       ForwardedHeaders.XForwardedHost,
    KnownNetworks = { }, KnownProxies = { }, ForwardLimit = null
});

// 2️⃣ Ensure HTTPS inside cluster
app.Use(async (ctx, next) =>
{
    if (!ctx.Request.IsHttps)
    {
        ctx.Response.Redirect($"https://{ctx.Request.Host}{ctx.Request.Path}{ctx.Request.QueryString}", false);
        return;
    }
    await next();
});

app.UseHttpsRedirection();
// … ABP default pipeline continues …

What we tried & outcome

| Attempt | Outcome | | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | Persist keys + SetApplicationName | ✅ Key‑ring loads without “key not found”. | | PostConfigure<DataProtectionOptions> last | ✅ Logs show application isolator: YourSite. | | UseForwardedHeaders first | ✅ Trace says Request IsHttps = True. | | Antiforgery header & secure cookie config | ❌ Still get 400 & “cookie token and request token were swapped”. | | Temporarily set CookieSecurePolicy = None | Browser blocks cookie (SameSite=None requires Secure) – still 400. | | Confirmed via DevTools: request sends no XSRF‑TOKEN cookie; only the form header. | |

Questions for ABP Support

  1. Is there ABP middleware in Blazor‑Hybrid that rewrites or clears the antiforgery cookie after UseForwardedHeaders, causing the browser to miss it?
  2. Does the Account Public login page in 9.2.2 expect a different antiforgery header configuration than the one we applied?
  3. Is any additional ABP setting required when running behind Envoy (ACA) to keep the XSRF‑TOKEN cookie?
  4. How can we enable even more detailed tracing for ABP’s antiforgery pipeline to see why the cookie is not returned?
  5. Is ABP explicitly expecting a different antiforgery token pattern or explicitly using an internal header?
  6. Could the separate Elsa Studio WASM app (added _Host.cshtml) under Elsa interfere with antiforgery for Account routes?

ABP Suite is creating lot of code with warnings!

then there are still (I have reported this 2x) spaces here and there through out the generated code

and so many other issues like this..

Why isn´t there more love for craftmanship in this product?

There is a bug Blazor razor pages when generating property of nullable Guid with ABP Suite

error CS1503: Argument 1: cannot convert from 'System.Guid?' to 'string'
error CS1503: Argument 2: cannot convert from 'method group' to 'Microsoft.AspNetCore.Components.EventCallback'

So instead of

do something like this

Are there to few tests with ABP Studio?

Showing 11 to 20 of 259 entries
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.1.0-preview. Updated on October 30, 2025, 06:33