Back

Access Management in B2B vs. B2C SaaS using Entra ID

Last week I presented a new session at Cloudbrew called Access Management in B2B vs. B2C SaaS: Similarities, Differences, and Best Practices, covering my experience in building and securing both B2B and B2C SaaS solutions. The session was very well received, so I hope to give it a few more times the coming months.

Without writing out my whole session, I wanted to share a few key points to help you move forward as I still see this often done wrong. The information and different types of implementation are also somewhat spread out over the Microsoft docs.

Concepts of a SaaS application

We can always ask ChatGPT to help us understand Software-as-a-Service (SaaS). I took a few fragments from the response to get us started:

Software as a Service (SaaS) is a software distribution model in which applications are hosted and provided to customers over the internet on a subscription basis. Instead of users installing and maintaining the software on their local devices or servers, they access and use the software through a web browser. Key characteristics of SaaS software include:

  1. Centralized Hosting: The software is hosted on a centralized server or cloud infrastructure, managed by the SaaS provider.

  2. Multi-Tenancy: SaaS applications are designed to serve multiple customers (tenants) on the same infrastructure. The provider ensures data isolation and security measures to maintain the privacy of each customer’s data.

The take-away here in function of this blog post is a single hosted application with multiple customers.

Different kinds of tenancy

When creating a new app registration, you immediately get asked which account types you want to support. The portal already gives you a bit of a help to decide, but the documentation page on supported account types goes into more detail.

These account types implicitly map to a tenancy model:

  • Single tenant: The application exists in one single Microsoft Entra ID tenant. Users are from your own organization only, or also guest users invited into your Microsoft Entra ID tenant.
    Note that Microsoft has been working on External Identities to enable collaboration on data within your tenant. Keep an eye on it as this evolves.

External identities

However, when building a SaaS solution, you don’t want to limit yourself to a single organization. Inviting every single user in your main tenant is a very bad idea, so what else is possible?

  • Multi-tenant B2B: The application ‘knows’ in which tenant the user resides and leverages this knowledge to correctly segregate data. The application lives in each corporate tenant (more details in the next paragraph).

Multi-tenant B2B

  • Multi-tenant B2C: In this scenario, we’re no longer speaking of organizations as customers, but individuals (with a corporate or private account). Since individuals might even be using another social login (Google, Facebook, …), we no longer have an Entra ID tenant to provision our application in. We manage all users in a separate directory called Azure AD B2C.

Note: Multi-tenant B2C users are currently still managed in Azure Active Directory B2C rather than Microsoft Entra ID. As you notice, this resource has not been renamed to Entra to clearly show the difference. In the future, Entra External ID will probably replace Azure AD B2C.

Microsoft also has a very detailed comparison on feature sets for the different B2B and B2C collaboration options.

What are app registrations?

Fellow MVP Emily van Putten has written a nice blog post on App Registrations and Enterprise Applications, so I’ll happily link to that one.

For those who want a TL;DR:

  • App Registration: This is the ‘contract’ of your API/frontend application and is created in your SaaS tenant (where you plan to hose the resources).
  • Enterprise Application: This is your ‘object instance’ which will be deployed in each B2B customer tenant.

App registration and enterprise application

More information on the flow how these instances get deployed in each tenant, can be found on the Microsoft docs: enable multi-tenant login.

Securing our .NET API

When building the demos for Cloudbrew, I ran into a few issues where code that I used in previous versions (.NET 5 and 6) was working purely for authentication, but was not correctly reading the app roles claims from my tokens. In the end I got everything working again, still using Microsoft.Identity.Web. If you’d run into issues, feel free to check out the full code on GitHub.

Note: while each project stands on its own to keep as close as possible to a new project, it is only the AuthenticationExtensions.cs and the appsettings.json files that are different.

Note: You will have to update all tenant and application ids to your needs. The ids in the sample code are from 3 brand-new tenants I did set up for demo purposes only, as I typically switch between commits for a more fluent demo and thus have ids in source control.

Single-tenant API

The easiest way to start is with a single tenant API. This uses an app registration for the API with organization-only accounts and a second app registration for the Swagger UI as ‘client’.

Let’s start with the appsettings.json file. If you’ve secured your API with Microsoft Entra ID before, you have most likely used this method of defining an AzureAd section.

  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<tenantname>.onmicrosoft.com",
    "TenantId": "<tenant id>",
    "ClientId": "<api app client id>"
  }

For internal applications you almost certainly want to work with roles and I find it easier to manage these roles through security groups in Microsoft Entra ID rather than roll your own role management in the application. For that reason we need to tell ASP.NET to read these claims.

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    services.AddMicrosoftIdentityWebApiAuthentication(configuration);
    services.Configure<OpenIdConnectOptions>(
        OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            // The claim in the Jwt token where App roles are available.
            options.TokenValidationParameters.RoleClaimType = "roles";
            options.TokenValidationParameters.NameClaimType = "name";
        });

    // The following lines code instruct the asp.net core middleware to use the data in the "roles" claim in the Authorize attribute and User.IsInrole()
    // See https://docs.microsoft.com/aspnet/core/security/authorization/roles?view=aspnetcore-2.2 for more info.
    services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
        // The claim in the Jwt token where App roles are available.
        options.TokenValidationParameters.RoleClaimType = "roles";
    });

Of course we need a client, so configure these endpoints in the client to correctly access your API. Again, don’t forget to update ids.

  "AuthorizationUrl": "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/authorize",
  "TokenUrl": "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token",

Multi-tenant B2B API

As explained above, in a multi-tenant application we define our app registration once in our SaaS home tenant, and it will be provisioned as an enterprise application in each consumer tenant.

If you changed your app registration to be multi-tenant, then there are no changes in appsettings.json for the API. As I had to give multiple demos, I created a new app registration, so I had to update that single id.

However on the client side, there are some changes: we no longer specify the tenant id, but replace that with the organizations string.

  "AuthorizationUrl": "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize",
  "TokenUrl": "https://login.microsoftonline.com/organizations/oauth2/v2.0/token",

In the code we have to pay attention to a few things:

  • Since every single customer tenant is a potential token issuer now, we no longer validate the issuer against our home tenant id.
  • However, you want to keep things secure, so rather than checking the issuer, we check the audience (= target recipient) to make sure the token is intended for our API.
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(jwtBearerOptions =>
            {
                // disable so we don't get issuer null error
                jwtBearerOptions.TokenValidationParameters.ValidateIssuer = false; // or write own validation to check vs your customer list
                
                // make sure it comes from our app registration
                jwtBearerOptions.TokenValidationParameters.ValidAudience = $"api://{configuration.GetSection("AzureAd")["ClientId"]}";
            }, identityOptions =>
            {
                configuration.Bind("AzureAd", identityOptions);
            }
        );

Multi-tenant B2C API

For B2C we use Azure Active Directory B2C, which is a totally different system than Microsoft Entra ID. So not only the instance name is different, it uses b2clogin.com and user flows or policies.

  "AzureAdB2C": {
    "Instance": "https://<tenant name>.b2clogin.com/",
    "Domain": "<tenant name>.onmicrosoft.com",
    "ClientId": "<api client id>",
    "SignedOutCallbackPath": "/signout/B2C_1_susi_reset_v2",
    "SignUpSignInPolicyId": "B2C_1_sign_up" // your policy name
  }

This means that the endpoints for the client also change:

  "AuthorizationUrl": "https://<tenant name>.b2clogin.com/<tenant name>.onmicrosoft.com/B2C_1_sign_up/oauth2/v2.0/authorize",
  "TokenUrl": "https://<tenant name>.b2clogin.com/<tenant name>.onmicrosoft.com/B2C_1_sign_up/oauth2/v2.0/token",

Finally in the code, I no longer care about roles but I do care about the name of the person logged in.

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(jwtBearerOptions =>
            {
                configuration.Bind("AzureAdB2C", jwtBearerOptions);

                jwtBearerOptions.TokenValidationParameters.NameClaimType = "name";
            }, identityOptions =>
            {
                configuration.Bind("AzureAdB2C", identityOptions);
            }
        );

Note: if for some reason you want B2C users to be able to work with roles as well, you either have manage it yourself with Microsoft Graph and custom policies, or you use a third-party identity provider that does this out of the box (like Auth0).

Conclusion

We did not go very deep into what you should and shouldn’t do when building a SaaS regarding to access and identity management. For that you can always invite me over for giving a presentation.

However, I hope that this blog posts give you a bit of an idea of the differences in both the concepts and implementation regarding to tenancy and target customers.

Licensed under CC BY-NC-SA 4.0; code samples licensed under MIT.
comments powered by Disqus
Built with Hugo - Based on Theme Stack designed by Jimmy