11/1/2020 Admin

Blazor and Azure B2C: The Big Picture


image

Azure Active Directory B2C is a service that allows your Blazor website users to log in using their preferred social, enterprise logins (or they can create a new local account in your Azure B2C tenant). Because this is a Azure Active Directory tenant, you have access to powerful features such as Multi Factor Authentication and Conditional Access control.

You can get more details at the following links:

The pricing is really affordable, basically you get the first 50,000 users for free.

Why Use Azure B2C?

image

All this sounds good, but, why would you want to use Azure B2C rather than just logging users directly into your Azure tenant? Multi-Tenant Azure login has been available for years, and you can easily log in users using external providers such as Google and Facebook.

 

image

The key difference is that with Azure B2C, all your users, regardless of how they were authenticated, are contained (and controlled) in your Azure B2C tenant, allowing you (or your IT department) to apply conditional access policies. These are essential for providing the tightest security and user management. It is also easier for developers to code against because they are coding against one identity end-point rather than several.

 

Configuring Blazor to use Azure B2C (Using User Flows)

image

The easiest way to get started with Blazor and Azure B2C is to simply follow the directions in this article (that implements User Flows):

Creating A Blazor Server Azure B2C App

 

Register Azure AD B2C

See: Tutorial - Create an Azure Active Directory B2C tenant | Microsoft Docs

image

 

Configure Azure AD B2C Identity Provider & User Flow

See: Tutorial - Create user flows - Azure Active Directory B2C | Microsoft Docs

image

 

Register Apps in Azure AD B2C tenant

See: Tutorial: Register an application - Azure AD B2C | Microsoft Docs

image

 

Create a Blazor App and Enjoy!

image

Note: You can also use the directions here (but they are not for Server Side Blazor applications only Client Side Blazor applications):

 

Allow users from Any Azure AD Tenant To Log In

If the following login providers are sufficient, you are good to go at this point:

image

However, the provider for Microsoft Azure AD is not in the list.

You can enable it by following the directions at this link: Set up sign-in for an Azure AD organization - Azure AD B2C | Microsoft Docs. However, you have to set up each Azure tenant you want to provide access to one at a time.

If you desire to allow any Azure AD tenant to log in, you need to use Custom Policies.

The advantages of using Custom Policies vs. User Flows is described here:

Comparing user flows and custom policies

 

Custom Policies

image

To use Custom Policies, the first step is to complete all the steps described here:

Get started with custom policies - Azure AD B2C | Microsoft Docs

This describes how to complete the following steps:

image

(1) Add signing and encryption keys

 

image

(2) Register Identity Experience Framework applications

 

You can now create a Custom Policies that will allow multi-tenant Azure AD login. To do that, complete the steps described here:

Set up sign-in for multi-tenant Azure AD by custom policies - Azure AD B2C | Microsoft Docs

 

To understand the details of the Custom Policies (basically the .xml format) see this link:

A Walkthrough For Azure AD B2C Custom Policy (Identity Experience Framework) 

The official documentation is at this link:

Reference - trust frameworks in Azure Active Directory B2C | Microsoft Docs

 

Pass an access token through a custom policy

image

When using Azure Active Directory authentication, to get additional information on the user, the idp_access_token needs to be passed to your application code using the following directions: Pass an access token through a custom policy to your app - Azure AD B2C | Microsoft Docs.

That token allows access to additional information in the user’s Azure AD tenant.

 

Implementing Multi-Tenant Azure B2C in Blazor Simple Survey

image

Blazor Simple Survey is an open source Github project that demonstrates integrating Azure B2C in a Blazor Server Side application.

The application currently features code that logs a user in and stores their information in the SQL database for the application.

You can see a live example of the application at the following link: BlazorSimpleSurvey

image

You can log into the application using the Log In link.

 

image

You will be presented with several login options, including the Multi-Tenant Azure Active Directory option.

 

image

Once logged in, click Auth Claims to see the information the application is able to retrieve.

 

Implementing Custom Policies in Blazor Simple Survey

image

For Blazor Simple Survey, the desire was to allow any Azure AD tenant to log in so Custom Policies had to be implemented.

Primarily this required the configuration of the Identity Experience Framework that is described here:  Get started with custom policies - Azure AD B2C | Microsoft Docs.

image

The end result is a series of custom .xml policies that allow login using the following providers:

  • Azure Active Directory Multi-Tenant
  • Google
  • Twitter
  • Microsoft Accounts

 

image

To create your own custom policies, you can start with the Custom policy starter pack available at this link: Get started with custom policies - Azure AD B2C | Microsoft Docs.

Or, you can access the custom policy files used for Blazor Simple Survey at the following location: BlazorSimpleSurvey/!AzureB2CConfig.

 

image

If using the custom policy files used for Blazor Simple Survey, you will have to update the files with the configuration values of your Azure B2C tenant.

Using an .xml editor like Visual Studio Code is recommended for editing the files.

 

image

If there are providers that are not desired (or providers you want to add), the TrustFrameworkExtensions.xml file can be edited to remove (or add) the provider.

 

image

Also remove (or add) a reference to the provider in the UserJourney section.

 

 

See this link for additional details: Get started with custom policies - Azure AD B2C | Microsoft Docs.

 

Implementing Custom Policies

image

There are three types of policy files:

  • Base File – This file contains most of the .xml policy definitions needed. You will make a minimum number of changes to this file.  
  • Extensions File – This file builds on top of the Base file and contains most of the unique configurations for your tenant. This is where you define the authentication providers such as Facebook, Twitter, and Azure multi-tenant authentication.
  • Relying Party (RP) File – This is a single task-focused file that is invoked directly by the application or service (also, known as a Relying Party). Each unique task requires its own RP.

 

image

The files use the Inheritance Model which is basically:

  • The application calls the Relying Party (RP) File, the Identity Experience Framework in Azure AD B2C adds all of the elements from the Base File, then the Extensions File, and finally the Relying Party (RP) File, to assemble the current policy (and features) to implement.
  • Elements in the files, of the same type (and name) in the Relying Party (RP) File will override those elements in the Extensions File, and elements in the Extensions File, will override elements of the same type and name in the Base File.
  • The PolicyId of the Relying Party (RP) File, is referenced in the Blazor application’s SignUpSignInPolicyId property in the appsettings.json file, to trigger the custom flow.

The login to the application is triggered by the Relying Party (RP) file. For Blazor Simple Survey, this is called: B2C_1A_signup_signin_AAD.

image

We then updated the SignUpSignInPolicyId property in the appsettings.json file with the name of this policy, so that policy would be triggered when a user clicked the Log In link.

 

Logging A User Into a Blazor Application And Storing Their Information In The Local SQL Server

image

For Blazor Simple Survey, a Users and a Logs table were created in the local SQL database for the application, and an Entity Framework Core DataContext was created.

In the Startup.cs file, the following code is added to the ConfigureServices method to gather the login values and save (or update them) when the user logs in through Azure B2C:

 

// This is where you wire up to events to detect when a user logs in
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(options =>
    {
        Configuration.Bind("AzureAdB2C", options);
        options.Events = new OpenIdConnectEvents
        {
            OnRedirectToIdentityProvider = async ctxt =>
            {
                // Invoked before redirecting to the identity provider to authenticate. 
                // This can be used to set ProtocolMessage.State
                // that will be persisted through the authentication process. 
                // The ProtocolMessage can also be used to add or customize
                // parameters sent to the identity provider.
                await Task.Yield();
            },
            OnAuthenticationFailed = async ctxt =>
            {
                // They tried to log in but it failed
                await Task.Yield();
            },
            OnTicketReceived = async ctxt =>
            {
                if (ctxt.Principal.Identity is ClaimsIdentity identity)
                {
                    // Set common values
                    AuthClaims objAuthClaims = new AuthClaims();
                    var colClaims = await ctxt.Principal.Claims.ToDynamicListAsync();
                    objAuthClaims.IdentityProvider = colClaims.FirstOrDefault(
                        c => c.Type == 
                        "http://schemas.microsoft.com/identity/claims/identityprovider")?.Value;
                    objAuthClaims.Objectidentifier = colClaims.FirstOrDefault(
                        c => c.Type == 
                        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value;
                    objAuthClaims.EmailAddress = colClaims.FirstOrDefault(
                        c => c.Type == 
                        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value;
                    objAuthClaims.FirstName = colClaims.FirstOrDefault(
                        c => c.Type == 
                        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname")?.Value;
                    objAuthClaims.LastName = colClaims.FirstOrDefault(
                        c => c.Type == 
                        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname")?.Value;
                    objAuthClaims.AzureB2CFlow = colClaims.FirstOrDefault(
                        c => c.Type == 
                        "http://schemas.microsoft.com/claims/authnclassreference")?.Value;
                    objAuthClaims.auth_time = colClaims.FirstOrDefault(
                        c => c.Type == "auth_time")?.Value;
                    objAuthClaims.DisplayName = colClaims.FirstOrDefault(
                        c => c.Type == "name")?.Value;
                    objAuthClaims.idp_access_token = colClaims.FirstOrDefault(
                        c => c.Type == "idp_access_token")?.Value;
                    // Google login
                    if (objAuthClaims.IdentityProvider.ToLower().Contains("google"))
                    {
                        objAuthClaims.AuthenticationType = "Google";
                    }
                    // Microsoft account login
                    if (objAuthClaims.IdentityProvider.ToLower().Contains("live"))
                    {
                        objAuthClaims.AuthenticationType = "Microsoft";
                    }
                    // Twitter login
                    if (objAuthClaims.IdentityProvider.ToLower().Contains("twitter"))
                    {
                        objAuthClaims.AuthenticationType = "Twitter";
                    }
                    // Azure Active Directory login
                    // But this will only work if Azure B2C Custom Policy is configured
                    // to pass the idp_access_token
                    // See \!AzureB2CConfig\TrustFrameworkExtensions.xml
                    // for an example that does that
                    if (objAuthClaims.idp_access_token != null)
                    {
                        objAuthClaims.AuthenticationType = "Azure Active Directory";
                        try
                        {
                            var token = 
                            new System.IdentityModel.Tokens.Jwt.
                            JwtSecurityToken(objAuthClaims.idp_access_token);
                            objAuthClaims.EmailAddress = 
                            token.Claims.FirstOrDefault(c => c.Type == "upn")?.Value;
                        }
                        catch (System.Exception)
                        {
                            // Could not decode - do nothing 
                        }
                    }
                    var request = ctxt.HttpContext.Request;
                    var host = request.Host.ToUriComponent();
                    // Insert into Database
                    var optionsBuilder = new DbContextOptionsBuilder<SimpleSurveyContext>();
                    optionsBuilder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
                    SimpleSurveyContext _context = new SimpleSurveyContext(optionsBuilder.Options);
                    var ExistingUser = _context.Users
                    .Where(x => x.Objectidentifier == objAuthClaims.Objectidentifier)
                    .FirstOrDefault();
                    if (ExistingUser == null)
                    {
                        // New User
                        // Create User object
                        var objUser = new Users();
                        try
                        {
                            objUser.Objectidentifier = objAuthClaims.Objectidentifier;
                            objUser.AuthenticationType = objAuthClaims.AuthenticationType;
                            objUser.IdentityProvider = objAuthClaims.IdentityProvider;
                            objUser.SigninMethod = objAuthClaims.AzureB2CFlow;
                            objUser.DisplayName = objAuthClaims.DisplayName;
                            objUser.Email = objAuthClaims.EmailAddress;
                            objUser.FirstName = objAuthClaims.FirstName;
                            objUser.LastName = objAuthClaims.LastName;
                            objUser.LastAuthTime = Convert.ToInt32(objAuthClaims.auth_time);
                            objUser.LastidpAccessToken = objAuthClaims.idp_access_token;
                            objUser.LastIpaddress = host;
                            objUser.CreatedDate = DateTime.Now;
                            _context.Users.Add(objUser);
                            _context.SaveChanges();
                            // Write to Log
                            var objLogs = new Logs();
                            objLogs.LogType = "Login";
                            objLogs.LogDate = DateTime.Now;
                            objLogs.LogDetail = "New User";
                            objLogs.LogUserId = objUser.Id;
                            objLogs.LogIpaddress = host;
                            _context.Logs.Add(objLogs);
                            _context.SaveChanges();
                        }
                        catch (Exception ex)
                        {
                            // Write to Log
                            var objLogs = new Logs();
                            objLogs.LogType = "Login Error - New User";
                            objLogs.LogDate = DateTime.Now;
                            objLogs.LogDetail = 
                            String.Format($"User: {objUser.DisplayName} " +
                            $"Objectidentifier: {objUser.Objectidentifier} " +
                            $"Message: {ex.GetBaseException().Message}");
                            objLogs.LogIpaddress = host;
                            _context.Logs.Add(objLogs);
                            _context.SaveChanges();
                        }
                    }
                    else
                    {
                        // Update Existing User
                        try
                        {
                            ExistingUser.AuthenticationType = objAuthClaims.AuthenticationType;
                            ExistingUser.IdentityProvider = objAuthClaims.IdentityProvider;
                            ExistingUser.SigninMethod = objAuthClaims.AzureB2CFlow;
                            ExistingUser.DisplayName = objAuthClaims.DisplayName;
                            ExistingUser.Email = objAuthClaims.EmailAddress;
                            ExistingUser.FirstName = objAuthClaims.FirstName;
                            ExistingUser.LastName = objAuthClaims.LastName;
                            ExistingUser.LastAuthTime = Convert.ToInt32(objAuthClaims.auth_time);
                            ExistingUser.LastidpAccessToken = objAuthClaims.idp_access_token;
                            ExistingUser.LastIpaddress = host;
                            ExistingUser.UpdatedDate = DateTime.Now;
                            _context.SaveChanges();
                            // Write to Log
                            var objLogs = new Logs();
                            objLogs.LogType = "Login";
                            objLogs.LogDate = DateTime.Now;
                            objLogs.LogDetail = "Existing User";
                            objLogs.LogUserId = ExistingUser.Id;
                            objLogs.LogIpaddress = host;
                            _context.Logs.Add(objLogs);
                            _context.SaveChanges();
                        }
                        catch (Exception ex)
                        {
                            // Write to Log
                            var objLogs = new Logs();
                            objLogs.LogType = "Login Error - Existing User";
                            objLogs.LogDate = DateTime.Now;
                            objLogs.LogUserId = ExistingUser.Id;
                            objLogs.LogDetail = ex.GetBaseException().Message;
                            objLogs.LogIpaddress = host;
                            _context.Logs.Add(objLogs);
                            _context.SaveChanges();
                        }
                    }
                }
                await Task.Yield();
            },
        };
    });
services.AddControllersWithViews().AddMicrosoftIdentityUI();      

 

Also See

Creating A Blazor Server Azure B2C App

Creating A Blazor WebAssembly Azure B2C Application

Blazor Azure B2C User And Group Management

Blazor Simple Survey: Creating Dynamic Surveys

 

Links

ADefWebserver/BlazorSimpleSurvey: Blazor Simple Survey (github.com)

Azure Active Directory B2C documentation | Microsoft Docs

What is Azure Active Directory B2C? | Microsoft Docs

A Walkthrough For Azure AD B2C Custom Policy (Identity Experience Framework) – tsmatz (wordpress.com)

How to Add Authentication to Blazor App using Azure B2C (Video)

Azure AD B2C Community (github.com) and azure-ad-b2c/samples: Azure AD B2C Identity Experience Framework sample User Journeys. (github.com)

Secure an ASP.NET Core Blazor WebAssembly hosted app with Azure Active Directory B2C | Microsoft Docs

Everything you wanted to know about Azure AD B2C custom policy samples but were afraid to ask!

Azure AD B2C OnTicketReceived newUser Claim | C# Tutorials Blog (wellsb.com)

An error has occurred. This application may no longer respond until reloaded. Reload 🗙