This article shows how to create different types of Azure B2C users using Microsoft Graph and ASP.NET Core. The users are created using application permissions in an Azure App registration.
Code https://github.com/damienbod/azureb2c-fed-azuread
The Microsoft.Identity.Web Nuget package is used to authenticate the administrator user that can create new Azure B2C users. An ASP.NET Core Razor page application is used to implement the Azure B2C user management and also to hold the sensitive data.
public void ConfigureServices(IServiceCollection services) { services.AddScoped<MsGraphService>(); services.AddTransient<IClaimsTransformation, MsGraphClaimsTransformation>(); services.AddHttpClient(); services.AddOptions(); services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C") .EnableTokenAcquisitionToCallDownstreamApi() .AddInMemoryTokenCaches();
The AzureAdB2C app settings configures the B2C client. An Azure B2C user flow is implemented for authentication. In this example, a signin or signup flow is implemented, although if creating your own user, maybe only a signin is required. The GraphApi configuration is used for the Microsoft Graph application client with uses the client credentials flow. A user secret was created to access the Azure App registration. This secret is stored in the user secrets for development and stored in Azure Key Vault for any deployments. You could use certificates as well but this offers no extra security unless using directly from a client host.
"AzureAdB2C": { "Instance": "https://b2cdamienbod.b2clogin.com", "ClientId": "8cbb1bd3-c190-42d7-b44e-42b20499a8a1", "Domain": "b2cdamienbod.onmicrosoft.com", "SignUpSignInPolicyId": "B2C_1_signup_signin", "TenantId": "f611d805-cf72-446f-9a7f-68f2746e4724", "CallbackPath": "/signin-oidc", "SignedOutCallbackPath ": "/signout-callback-oidc" }, "GraphApi": { "TenantId": "f611d805-cf72-446f-9a7f-68f2746e4724", "ClientId": "1d171c13-236d-4c2b-ac10-0325be2cbc74", "Scopes": ".default" //"ClientSecret": "--in-user-settings--" }, "AadIssuerDomain": "damienbodhotmail.onmicrosoft.com",
The application User.ReadWrite.All permission is used to create the users. See the permissions in the Microsoft Graph docs.
The MsGraphService service implements the Microsoft Graph client to create Azure tenant users. Application permissions are used because we use Azure B2C. If authenticating using Azure AD, you could use delegated permissions. The ClientSecretCredential is used to get the Graph access token and client with the required permissions.
public MsGraphService(IConfiguration configuration) { string[] scopes = configuration.GetValue<string>("GraphApi:Scopes")?.Split(' '); var tenantId = configuration.GetValue<string>("GraphApi:TenantId"); // Values from app registration var clientId = configuration.GetValue<string>("GraphApi:ClientId"); var clientSecret = configuration.GetValue<string>("GraphApi:ClientSecret"); _aadIssuerDomain = configuration.GetValue<string>("AadIssuerDomain"); _aadB2CIssuerDomain = configuration.GetValue<string>("AzureAdB2C:Domain"); var options = new TokenCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud }; // https://docs.microsoft.com/dotnet/api/azure.identity.clientsecretcredential var clientSecretCredential = new ClientSecretCredential( tenantId, clientId, clientSecret, options); _graphServiceClient = new GraphServiceClient(clientSecretCredential, scopes); }
The CreateAzureB2CSameDomainUserAsync method creates a same domain Azure B2C user and also creates an initial password which needs to be updated after a first signin. The users UserPrincipalName email must match the Azure B2C domain and the users can only signin with the the password. MFA should be setup. This works really good but it is not a good idea to handle passwords from your users, if this can be avoided. You need to share this with the user in a secure way.
public async Task<(string Upn, string Password, string Id)> CreateAzureB2CSameDomainUserAsync(UserModelB2CTenant userModel) { if(!userModel.UserPrincipalName.ToLower().EndsWith(_aadB2CIssuerDomain.ToLower())) { throw new ArgumentException("incorrect Email domain"); } var password = GetEncodedRandomString(); var user = new User { AccountEnabled = true, UserPrincipalName = userModel.UserPrincipalName, DisplayName = userModel.DisplayName, Surname = userModel.Surname, GivenName = userModel.GivenName, PreferredLanguage = userModel.PreferredLanguage, MailNickname = userModel.DisplayName, PasswordProfile = new PasswordProfile { ForceChangePasswordNextSignIn = true, Password = password } }; await _graphServiceClient.Users .Request() .AddAsync(user); return (user.UserPrincipalName, user.PasswordProfile.Password, user.Id); }
The CreateFederatedUserWithPasswordAsync method creates an Azure B2C with any email address. This uses the SignInType federated, but uses a password and the user signs in directly to the Azure B2C. This password is not updated after a first signin. Again this is a bad idea because you need share the password with the user somehow and you as an admin should not know the user password. I would avoid creating users in this way and use a custom invitation flow, if you need this type of Azure B2C user.
public async Task<(string Upn, string Password, string Id)> CreateFederatedUserWithPasswordAsync(UserModelB2CIdentity userModel) { // new user create, email does not matter unless you require to send mails var password = GetEncodedRandomString(); var user = new User { DisplayName = userModel.DisplayName, PreferredLanguage = userModel.PreferredLanguage, Surname = userModel.Surname, GivenName = userModel.GivenName, OtherMails = new List<string> { userModel.Email }, Identities = new List<ObjectIdentity>() { new ObjectIdentity { SignInType = "federated", Issuer = _aadB2CIssuerDomain, IssuerAssignedId = userModel.Email }, }, PasswordProfile = new PasswordProfile { Password = password, ForceChangePasswordNextSignIn = false }, PasswordPolicies = "DisablePasswordExpiration" }; var createdUser = await _graphServiceClient.Users .Request() .AddAsync(user); return (createdUser.UserPrincipalName, user.PasswordProfile.Password, createdUser.Id); }
The CreateFederatedNoPasswordAsync method creates an Azure B2C federated user from a specific Azure AD domain which already exists and no password. The user can only signin using a federated signin to this tenant. No passwords are shared. This is really good way to onboard existing AAD users to an Azure B2C tenant. One disadvantage with this is that the email is not verified unlike implementing this using an invitation flow directly in the Azure AD tenant.
public async Task<string> CreateFederatedNoPasswordAsync(UserModelB2CIdentity userModel) { // User must already exist in AAD var user = new User { DisplayName = userModel.DisplayName, PreferredLanguage = userModel.PreferredLanguage, Surname = userModel.Surname, GivenName = userModel.GivenName, OtherMails = new List<string> { userModel.Email }, Identities = new List<ObjectIdentity>() { new ObjectIdentity { SignInType = "federated", Issuer = _aadIssuerDomain, IssuerAssignedId = userModel.Email }, } }; var createdUser = await _graphServiceClient.Users .Request() .AddAsync(user); return createdUser.UserPrincipalName; }
When the application is started, you can signin as an IT admin and create new users as required. The Birthday can only be added if you have an SPO license. If the user exists in the AAD tenant, the user can signin using the federated identity provider. This could be improved by adding a search of the users in the target tenant and only allowing existing users.
Notes:
It is really easy to create users using Microsoft Graph but this is not always the best way, or a secure way of onboarding new users in an Azure B2C tenant. If local data is required, this can be really useful. Sharing passwords between an IT admin and a new user should be avoided if possible. The Microsoft Graph invite APIs do not work for Azure AD B2C, only Azure AD.
Links
https://docs.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core
This content was originally published here.