9/14/2020 Admin

Blazor Multi-Tenant Azure B2C


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 link: Technical and feature overview - Azure Active Directory B2C | Microsoft Docs

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

Configuring Blazor to use Azure B2C

image

The easiest way to get started with Blazor and Azure B2C is to simply follow the directions in this article:

Azure AD B2C Quickstart with Visual Studio & Blazor | by Marcus Tee | Marcus Tee Anytime | Medium

(Note: You can also use the directions here (but they are not for Server Side Blazor applications only Client Side Blazor applications): Secure an ASP.NET Core Blazor WebAssembly hosted app with Azure Active Directory B2C | Microsoft Docs).

image

These directions were used for the initial version of the open source Blazor Simple Survey application and they work well.

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.

Custom Policies

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

Then complete the steps described here:

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

Note: 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.

image

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

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

Note: You can access the custom policy files used at the following location: BlazorSimpleSurvey/!AzureB2CConfig (you will have to update the files with the configuration values of your Azure B2C tenant – see this link for additional details: Get started with custom policies - Azure AD B2C | Microsoft Docs).

The login is triggered by the Relying Party (RP) file that was created, 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.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme, 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();
								
									},
								
									};
								
});

Links

Azure AD B2C Quickstart with Visual Studio & Blazor | by Marcus Tee | Marcus Tee Anytime | Medium

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)

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

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

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