Implementing Multiple Identities in your .NET Core Web App – Part 2

Last week we got started implementing multiple identities in a .NET Core web app by using Azure AD B2C:
https://blogs.msdn.microsoft.com/azuredev/2018/09/03/implementing-multiple-identities-in-your-net-core-web-app-part-1/

We were able to very quickly get going logging in with both social and corporate identities by clicking through guided wizards. However this approach fell short when we decided to support multiple Azure AD tenants without having to add every single one of them manually. The conclusion was basically that we need to up our game by using custom policies.

Azure AD B2C Custom policies
Let's pause for a moment before creating the policies we need.

Up until now the AAD B2C experience has been fairly easy going. You might not have a grasp on all the details, but the portal experience makes a lot of tasks achievable. We have after all built a web app letting us sign in with different identities and segmentation by role with a minimum of coding effort.

The portal experience is just a subset of AAD B2C though. Everything in AAD B2C builds on top of what Microsoft calls the Identity Experience Framework, or IEF for short. IEF is a claims engine where you can orchestrate a number of steps; displaying a UI, collecting info provided by the user, doing things on the back-end, and finally ending up with a token issued to your app. It's quite powerful. And quite complex… When dealing with the IEF you are not doing level 200 stuff.

This brings us back to one of the points I made in the beginning of the first post. Do you need this level of customization?

Since we have defined that partners should be able to use their AAD account to sign in we probably should look into custom policies. In fact, there are other aspects to the partner story that most likely will lead us towards custom policies too. If you don't have these needs though the basic signup and signin are covered quite well by built-in policies. (Do note that we're dealing with customization of logic here. If you're thinking that you need a custom UI that is a different topic which is addressable with both built-in and custom policies.)

If I were to give an A-Z walkthrough of custom policies we would be here for a long while, so I would refer you to the official docs for that:
/en-us/azure/active-directory-b2c/active-directory-b2c-get-started-custom

Mind you. Don't go through all of it now. Just go through the setup part of things:
- Register the AAD B2C apps.
- Download the starter pack and modify files to work with your tenant as instructed. (Adding client ids and tenant names.)
- Test the policy with Run now just like the built-in policies.

Skip the Next steps section as well.

It's better to follow that doc than me replicating the instructions here since that will be kept up to date (hopefully), and my instructions might be outdated before that. Return here after doing that part.

As a tip for working with the XML files - use Visual Studio Code and install the Visual Studio Code Azure AD B2C extension:
https://marketplace.visualstudio.com/items?itemName=AzureADB2CTools.aadb2c

That makes it easier to browse the different types of policies:

I went ahead and implemented the Common endpoint as instructed:
/en-us/azure/active-directory-b2c/active-directory-b2c-setup-commonaad-custom

Clicking Run now you will notice that there is now a multi-tenant AAD option:

If configured correctly it should work nicely to sign in through that.

But where are all the other things we already defined? We had an employee button, and a Google button. Yes, but those were defined in the built-in policy section of AAD B2C - they don't automatically carry over to the custom policies. Which means that we need to define these as claims providers as well in the XML.

Though we cannot cover every bit of detail of what goes into these files I think it could be helpful to do a super short intro to the elements of a custom policy.

Custom Policy Elements

User Journeys
These are as the name implies the high level user experience. This does not have to be visible to a user, but let's say a basic journey goes like this:
- Have user choose which identity provider to use.
- Have user type in credentials.
- Verify if we need to collect more info from the user.
- Validate an input field through an API.
- Issue token.

The journey is basically just a collection of links to the other elements of the custom policy.

Claims Providers
A claims provider is a component that is able to issue claims. This would be identity providers like Azure AD, Facebook, etc. but could also be a REST API. A claims provider will accept input as well outputting claims.

Technical Profiles
These are generic container elements.

Claim Types
The definitions of claims; both data type, display name, etc. If a type for a claim does not exist it cannot be used other places either.

Claims Transformations
How claims are transformed - for instance if a user object has three email addresses defined these can be transformed into a claim list called "emails".

Content Definitions
These are used for linking to custom html and css. You would use these for creating a branded experience. Note that you cannot currently use custom JavaScript in these files.

So, to get back to the setup we had with multiple providers to choose from you will need to add the proper XML definitions.

Add identity providers to custom policies

Adding MSA as an Identity Provider:
/en-us/azure/active-directory-b2c/active-directory-b2c-custom-setup-msa-idp

Adding Google as an Identity Provider:
/en-us/azure/active-directory-b2c/active-directory-b2c-custom-setup-goog-idp

Adding Azure AD as an OpenID Connect Identity Provider:
/en-us/azure/active-directory-b2c/active-directory-b2c-setup-aad-custom

As these docs show the basic flow for working with custom profiles is to edit one of three XML files, upload one or more files to the portal and test. Rinse and repeat until satisfied.

You don't refer to the base and extensions files in your app, so there's only one reference needed for policies. I called my policy B2C_1A_Signup_Signin_Dev.

Our code still references a built-in policy so you need to change this in your appsettings.json file:

 
"SignUpSignInPolicyId": "B2C_1A_Signup_Signin_Dev",

This should take you back to something similar to our initial authentication experience. You're able to sign in and get a greeting, but the authorization setup we created is gone too. We rely on the role attribute being present, but we don't have that any longer. What we need is:
- A custom claim type.
- A way to capture the value for this claim.
- A way to persist it to the user object.
- A way to emit the claim in the issued token.

We can do all of this in our custom policies, but the initial step is to grant access to custom attributes to our policy.

I'll borrow a couple of paragraphs from the Custom attributes doc:
/en-us/azure/active-directory-b2c/active-directory-b2c-create-custom-attributes-profile-edit-custom

Supporting custom attributes in custom policies

Create a new application to store the extension properties:
- Open a browsing session and navigate to the Azure portal. Sign in with the administrative credentials of the B2C directory you want to configure.
- Select Azure Active Directory on the left navigation menu. You might need to find it by selecting More services.
- Select App registrations. Select New application registration.
- Provide the following entries:
A name for the web application: WebApp-GraphAPI-DirectoryExtensions.
The application type: Web app/API.
The sign-on URL: https://{tenantName}.onmicrosoft.com/WebApp-GraphAPI-DirectoryExtensions.
- Select Create.
- Select the newly created web application.
- Select Settings > Required permissions.
- Select the API Windows Azure Active Directory.
- Enter a checkmark in Application Permissions: Read and write directory data. Then select Save.
- Choose Grant permissions and confirm Yes.
- Copy the following identifiers to your clipboard and save them:
Application ID. Example: 103ee0e6-f92d-4183-b576-8c3739027780.
Object ID. Example: 80d8296a-da0a-49ee-b6ab-fd232aa45201.

Add these guids to your Azure Active Directory claims provider

 
<ClaimsProviders>
  <ClaimsProvider>
    <DisplayName>Azure Active Directory</DisplayName>
    <TechnicalProfile Id="AAD-Common">
      <DisplayName>Azure Active Directory</DisplayName>
      <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.AzureActiveDirectoryProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
      <!-- Provide objectId and appId before using extension properties. -->
      <Metadata>
        <Item Key="ApplicationObjectId">insert objectId here</Item>
        <Item Key="ClientId">insert appId here</Item>
      </Metadata>
      <!-- End of changes -->
      <CryptographicKeys>
        <Key Id="issuer_secret" StorageReferenceId="TokenSigningKeyContainer" />
      </CryptographicKeys>
      <IncludeInSso>false</IncludeInSso>
      <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
    </TechnicalProfile>
  </ClaimsProvider>
</ClaimsProviders>

You can now add the following claim in the end of Section III in ClaimsSchema in TrustFrameworkBase.xml:

 
<ClaimType Id="extension_Role">
  <DisplayName>Role</DisplayName>
  <DataType>string</DataType>
  <UserHelpText>Your role.</UserHelpText>
  <UserInputType>DropdownSingleSelect</UserInputType>
  <Restriction>
    <Enumeration Text="Customer" Value="Customer" SelectByDefault="false" />
    <Enumeration Text="Employee" Value="Employee" SelectByDefault="false" />
    <Enumeration Text="Partner" Value="Partner" SelectByDefault="false" />
  </Restriction>
</ClaimType>

We chose to have a dropdown list since there is no need to have a free form textbox, but you can have different input types depending on what you need.

In TrustFrameworkBase.xml you need to add an output claim to:

 
<OutputClaim ClaimTypeReferenceId="extension_Role" />   

You need to add this output claim in B2C_1A_Signup_Signin_Dev.xml as well:

 
<OutputClaim ClaimTypeReferenceId="Role" PartnerClaimType="extension_Role" />

Upload the files, and cross your fingers that they validate.

If you sign in with the same account that you used previously you should have a role in your token, and the authorization still works as expected.

That is all nice and dandy, but we're not collecting this on sign up, and it's still open for everyone to select themselves what the value should be. For this to happen we need to delve further into claims. Custom policies have three claim types: InputClaim, OutputClaim and PersistentClaim.

An InputClaim is what goes into an orchestration step. If you have a REST API Claim Provider the input claim is what you send to the API. And OutputClaim is what comes out at the other end; for instance the returned values from an API.

This seems to be simple enough, but can be confusing. For instance, if we want a user to provide input into a textbox that is not an input claim. That is an output claim, because we want whatever they type in to go out of that specific step.

By default the claims are ephemeral. "Something" happens in the claims engine that emits a large or small number of claims. The claims exist in the token issued, but when you log out and delete your session they go away. This is perfectly logical for claims like the time stamp for issuance. If you collect extra info from the user on signup you probably only want them to enter this once, and have it retrieved automatically on subsequent sessions. This requires a PersistentClaim since the value is persisted to the user object.

With that understanding in place we can implement the role attribute properly.

Implementing roles in custom policies

The high level flow for this is the following:
- User selects the Identity Provider they want to use.
- User authenticates (external IdPs) or creates a password (local accounts).
- User object is created in AAD B2C.
- The role is selected by the user.
- The user object is updated.

Let's get our hands dirty writing some XML.

We have a user journey defined in TrustFrameworkExtensions.xml and we need to add another OrchestrationStep to this:

 
<OrchestrationStep Order="7" Type="ClaimsExchange">
  <ClaimsExchanges>
    <ClaimsExchange Id="AADUserUpdate" TechnicalProfileReferenceId="SelfAsserted-RoleUpdate" />
  </ClaimsExchanges>
</OrchestrationStep>

As you might be able to guess this is basically just a reference to a technical profile.

Make sure to update the Order of the steps coming next in the journey - you will need sequential numbers in order and will get an error when uploading if it isn't right.

We then need to define a profile that will update the user object with the role attribute. Open up TrustFrameworkBase.xml. Find the Self asserted claims provider, and add another technical profile:

 
<TechnicalProfile Id="SelfAsserted-RoleUpdate">
  <DisplayName>User ID signup</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <Metadata>
    <Item Key="ContentDefinitionReferenceId">api.selfasserted.profileupdate</Item>
  </Metadata>
  <IncludeInSso>false</IncludeInSso>
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="alternativeSecurityId" />
    <InputClaim ClaimTypeReferenceId="userPrincipalName" />
    <!-- Optional claims. These claims are collected from the user and can be modified. Any claim added here should be updated in the
         ValidationTechnicalProfile referenced below so it can be written to directory after being updateed by the user, i.e. AAD-UserWriteProfileUsingObjectId. -->            
    <InputClaim ClaimTypeReferenceId="extension_Role" />
  </InputClaims>
  <OutputClaims>
    <!-- Required claims -->
    <OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
    <!-- Optional claims. These claims are collected from the user and can be modified. Any claim added here should be updated in the
         ValidationTechnicalProfile referenced below so it can be written to directory after being updatee by the user, i.e. AAD-UserWriteProfileUsingObjectId. -->
    <OutputClaim ClaimTypeReferenceId="extension_Role" />
  </OutputClaims>
  <ValidationTechnicalProfiles>
    <ValidationTechnicalProfile ReferenceId="AAD-UserWriteProfileUsingObjectId" />
  </ValidationTechnicalProfiles>
</TechnicalProfile>

Locate the AAD-UserWriteProfileUsingObjectId technical profile, and add an element to the list of persisted claims:

 
<PersistedClaim ClaimTypeReferenceId="extension_Role" /> 

Sign up a new user, and you will notice that you get a new screen displaying a dropdown list of roles. This should work for all Identity Providers.

Now, that is sweet isn't it?
A custom claim type => Check
A way to capture the value for this claim => Check
A way to persist it to the user object => Check
A way to emit the claim in the issued token => Check

What's missing now? Let's see - it's still possible to register as a partner just by selecting Partner in the dropdown. We don't want that. There are many ways to solve this, but we'll keep this simple too. Let's say that a partner's email address has to be pre-registered in a database for this role to be selectable.

This basically calls for an API to perform a bit more logic than the policy itself can handle.

Employing REST APIs in custom policies

We pass in the email address and the selected role, and decide whether this is ok or not. Azure Functions is an easy way for us to do such things, so let's implement the following function:

 
#r "Newtonsoft.Json"

using System;
using System.Net;
using System.Net.Http.Formatting;
using Newtonsoft.Json;

public static async Task<object> Run(HttpRequestMessage request, TraceWriter log)
{
    log.Info($"Webhook was triggered!");

    string requestContentAsString = await request.Content.ReadAsStringAsync();
    dynamic requestContentAsJObject = JsonConvert.DeserializeObject(requestContentAsString);

    if (requestContentAsJObject.emailAddress == null)
    {
        return request.CreateResponse(HttpStatusCode.BadRequest);
    }

    var email = ((string) requestContentAsJObject.emailAddress).ToLower();
    var role = ((string) requestContentAsJObject.role).ToLower();

    log.Info($"email: {email}, role: {role}");

    char splitter = '@';
    string[] splitEmail = email.Split(splitter);
    var emailSuffix = splitEmail[1];

    if ((role == "partner" && email != "chuck@norris.com") || (role == "partner" &&  emailSuffix != "northwind.com"))
    {
        return request.CreateResponse<ResponseContent>(
            HttpStatusCode.Conflict,
            new ResponseContent
            {
                version = "1.0.0",
                status = (int) HttpStatusCode.Conflict,
                userMessage = $"Your account does not seem to be a partner account."
            },
            new JsonMediaTypeFormatter(),
            "application/json");
    }    

    return request.CreateResponse(HttpStatusCode.OK);
}

public class ResponseContent
{
    public string version { get; set; }
    public int status { get; set; }
    public string userMessage { get; set; }
}

Basically, one specific email address is whitelisted (chuck@norris.com), and one domain (northwind.com). This is to catch the use case for partners signing in with Azure AD tenants, and social accounts. (With no database on the backend this clearly doesn't scale, but the code should get the point across nonetheless.)

Copy off the Function URL for use in our policy.

Now we will add the API for validation, and this comes in the form of a claims provider in TrustFrameworkExtensions.xml:

 
<ClaimsProvider>
  <DisplayName>REST APIs</DisplayName>
  <TechnicalProfiles>
    <TechnicalProfile Id="CheckPartnerRole">
    <DisplayName>Verify that user is a partner account</DisplayName>
    <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    <Metadata>
      <Item Key="ServiceUrl">https://your-b2c-function.azurewebsites.net/api/CheckPartnerRole?code=fooSecret</Item>
      <Item Key="AuthenticationType">None</Item>
      <Item Key="SendClaimsIn">Body</Item>
    </Metadata>
    <InputClaims>
      <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="emailAddress" />
      <InputClaim ClaimTypeReferenceId="extension_Role" PartnerClaimType="role" />    
    </InputClaims>
    <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
    </TechnicalProfile>
    <TechnicalProfile Id="SelfAsserted-RoleUpdate">
      <ValidationTechnicalProfiles>
        <ValidationTechnicalProfile ReferenceId="CheckPartnerRole" />
      </ValidationTechnicalProfiles>
    </TechnicalProfile>
  </TechnicalProfiles>
</ClaimsProvider>

You might run into a weird issue here with getting a Bad Request in return depending on which identity provider you signed in with. Well, here's another thing to check. You pass in the following for the REST API provider:

 
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="emailAddress" />. 

PartnerClaimType is what the API expects, and email is what B2C expects to use as that value. But if you didn't map this correctly with regards to what your IdP calls it you don't have that claim to pass in. Of course, with a REST API you build yourself you can adjust things on the backend, but you cannot trust on that always being the case.

For the Active Directory ClaimsProvider the attribute with the email in it is called unique_name, and requires adding this line in TrustFrameworkExtensions.xml:

 
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="unique_name" />

If your address is not on the list you will get an error saying so:

To avoid the user having to be asked about this claim every time they login we make a minor adjustment to the orchestration step by adding preconditions:

 
<OrchestrationStep Order="7" Type="ClaimsExchange">
  <Preconditions>
    <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
      <Value>extension_Role</Value>
      <Value>Employee</Value>              
      <Action>SkipThisOrchestrationStep</Action>
    </Precondition>
      <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
        <Value>extension_Role</Value>
        <Value>Partner</Value>              
        <Action>SkipThisOrchestrationStep</Action>
      </Precondition>
      <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
        <Value>extension_Role</Value>
        <Value>Customer</Value>              
        <Action>SkipThisOrchestrationStep</Action>
      </Precondition>
    </Preconditions>
  <ClaimsExchanges>

This means we will check the value of the claim already present, and if it's good we'll skip further checks.

Without going further into the details of this you can probably appreciate that there are a lot of things you can do if you get creative here. This includes making this step invisible and not exposing a choice at all for the user.

But I can see all the providers and options when I sign in. I don't want a customer to be confused by having an Employee choice, even we prevent them from actually logging in through Azure AD. Can we manipulate the initial page somehow?

Adjusting the signup and signin experience

You can. Depending on the details of what you want things to look like there are two main methods for doing so.

One method would for instance be too create multiple custom policies and tweak the settings accordingly. Have one policy called CustomerSignUpSignIn, and another called EmployeeSignUpSignIn, and so on. This is very clean, but introduces some extra overhead with maintaining multiple policies, and possibly duplicating parts of the logic between them.

Another method is using what is called "domain hints". This is controlled by the the claims providers configuration. If you add domain_hint=Contoso to the end of the B2C url so it becomes like this:
https://login.microsoftonline.com/contosob2c.onmicrosoft.com/oauth2/v2.0/authorize?p=B2C\_1A\_Signup\_Signin
&client_id=guid&nonce=defaultNonce&redirect_uri=https%3A%2F%2Fjwt.ms
&scope=openid&response_type=id_token&prompt=login&domain_hint=Contoso

That will take you directly to the Contoso login. What you use for this value is up to you and configured by the value in the XML:

 
<ClaimsProvider>
  <Domain>Contoso</Domain>
  <DisplayName>Employee</DisplayName>
  <TechnicalProfiles>
    <TechnicalProfile Id="ContosoProfile">

By default this is not configured for local accounts, but you can add it for those too:

 
<ClaimsProvider>
  <Domain>localAccount</Domain>
  <DisplayName>Local Account</DisplayName>
  <TechnicalProfiles>

With built-in local account domain hint does not work, but social does. So if you add facebook.com as a domain hint it will work, but for local there is no hint to input.

Another hint you can use is called a login hint. This is used for passing the username to the policy, so instead of typing in email and password the user only has to enter the password. There are however two noteworthy restrictions you need to be aware of here:
- It is not available for sign up or password reset flow - it can only be used for sign in. (To prevent spambots I assume.)
- It is entirely up to the IdP if they support this. Which means you can use it for local accounts and Google, but it does not work for Facebook.

This means that it possible to create confusing UI like asking for the email address to send the user to the right IdP, and then have them re-typing their email address in the next step.

This also requires adding a couple of lines to TrustFrameworkBase.xml. Locate SelfAsserted-LocalAccountSignin-Email and adjust the signInName input claim:

 
<InputClaims>            
  <InputClaim ClaimTypeReferenceId="signInName" DefaultValue="{OIDC:LoginHint}" />
</InputClaims>

Since the availability of this feature is limited I'm just applying it to local accounts.

Whether you choose to use different policies, or domain hints you need to make a couple of adjustments in your web app. There is only one button for signing in currently, and there is only one policy configured for this purpose in appsettings.json. There isn't anything magic about these two properties though. The important part in your web app is where you end up after signing in - the /signin-oidc part of the url. This is where you app expects to get a token, and this is where AAD B2C expects to redirect to. The url triggering the flow can however be changed to your liking. Instead of having just the one generic button you can use multiple urls in your UI.

Button 1 - https://yourtenant/policy_1 => Partner Login
Button 2 - https://yourtenant/policy_2&domain_hint=contoso => Employee Login
Button 3 - https://yourtenant/policy_3&user_hint=foo@bar.com => Customer Login

You get the picture.

I've added some code to index.cshtml to illustrate this:

 
<div class="container">
  <div class="row">
    <hr />
    <div class="col">
      <a class="btn btn-default" href="https://login.microsoftonline.com/yourtenant.onmicrosoft.com/oauth2/v2.0/authorize?
       p=B2C_1A_Signup_Signin_Dev&client_id=guid&nonce=defaultNonce&redirect_uri=https%3A%2F%2Fjwt.ms&scope=openid
       &response_type=id_token&prompt=login&domain_hint=commonaad">Partner Login</a>
    </div>
    <div class="col">
      <hr/>
      <a class="btn btn-default" href="https://login.microsoftonline.com/yourtenant.onmicrosoft.com/oauth2/v2.0/authorize?
       p=B2C_1A_Signup_Signin_Dev&client_id=guid&nonce=defaultNonce&redirect_uri=https%3A%2F%2Fjwt.ms&scope=openid
       &response_type=id_token&prompt=login&domain_hint=Contoso">Employee Login</a>
    </div>
    <div class="col">
      <hr />
      <a class="btn btn-default" href="https://login.microsoftonline.com/yourtenant.onmicrosoft.com/oauth2/v2.0/authorize?
       p=B2C_1A_Signup_Signin_Dev&client_id=guid&nonce=defaultNonce&redirect_uri=https%3A%2F%2Fjwt.ms&scope=openid
       &response_type=id_token&prompt=login&domain_hint=localAccount&login_hint=foo@bar.com">Customer Login (bob@contoso.com)</a>
    </div>
  </div>
</div>

Full flexibility on that part as well in other words. Feel free mixing built-in and custom policies too if you like.

Since the app is intended for dev purposes I also added some cshtml for printing out the values in the current token.

 
@if (User.Identity.IsAuthenticated)
{
    <h2>Claims Present in the Claims Identity: @User.Identity.Name</h2>
    <table class="table">
        <thead>
            <tr>
                <th scope="col">Claim Type</th>
                <th scope="col">Claim Value</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var claim in User.Claims)
            {
                <tr>
                    <td>@claim.Type</td>
                    <td>@claim.Value</td>
                </tr>
            }
        </tbody>
    </table>
}

This ends our exploration of how to implement multiple identities in a .NET Core app. I would say it's not hard to see the value of using AAD B2C for this purpose instead of building it all into your app. Even if you only implement Azure AD as the sole identity the Identity Experience Framework adds an abstraction layer that is useful in many scenarios.

The complete sample is available on https://github.com/ahelland/AADGuide-CodeSamples/tree/master/B2CNETCoreWebApp, and I have included the custom policies there as well.