Home
Senior Member

We all love simplicity, yet we also love freedom of choice. Sometimes these two parameters can be at odds with each other. For instance, if I go out for a beer I'm not too happy if there's only one beer on tap (unless it happens to be one of my absolute favorites). If they have thirty different beers I face the problem of not being able to try them all out in an evening. Do a web search for "paradox of choice" for further explanations on this conundrum.

 

So, how does this relate to Azure AD B2C and authentication? Well, Azure AD B2C is after all a solution designed to handle more than one option for logging in to an app. But how is an end-user supposed to make the "right" choice when presented with five different options?

 

I've already done a deep-ish dive into Azure AD B2C and how you go about providing different options:

https://techcommunity.microsoft.com/t5/Azure-Developer-Community-Blog/Implementing-Multiple-Identiti...

 

You should hopefully be able to follow along with some of the concepts presented even without being familiar in depth with custom policies, but I am assuming you either know the basics already or you are able to learn the necessary details outside of this article. Either through the aforementioned walkthrough or the official docs.

 

In those posts I mentioned how you could send users off to specific identity providers either by using different policies or adding the domain hint parameter to the query string. In addition, since we added the Azure AD common endpoint it was possible to have some magic behind the scenes as well taking you to the right Azure AD tenant for signin.

 

With this post I intend to dive further into this specific topic that we often refer to as "home realm discovery" (I'll shorten this to HRD for the rest of this post.).

 

Why would you care about HRD?

Let's say Contoso has a contoso.com web page. When employees sign in they're likely to use their @contoso.com email addresses, and it's easy enough adding a button for that. But how do you ensure that users don't register a local account which is tied to their contoso.com email address, but not their existing Contoso identity object? Having HRD would let you override that to make sure they use the right provider.

 

Or in the case of consumers - in a basic Azure AD B2C setup we could present two buttons on the signup screen - "Google account" and "Facebook account". The user makes a choice, and when they return (if their session is expired) we would ask again which of the two you want to use for signing in. You could probably use cookies to keep track of what the user did the last time, but it wouldn't carry over to a new computer/device. Since we have this information stored backend it shouldn't be necessary to ask for more than the email address to figure it out. (Let's keep the fact that you can link multiple social identities to one B2C identity out of this context.)

 

Which mechanisms do we have at disposal to do things differently?

 

Hints in B2C query strings

First a repetition of the basics. In addition to offering the ability to define a number of policies Azure AD B2C can take in two parameters in the query string that can affect our choice of identity providers. You have "login_hint" which would be for prefilling the username (this could be both an email address or just "bob"), and there's "domain_hint" which would be a tag for a specific IdP. This means that if you use "login_hint=bob@contoso.com&domain_hint=contoso" the browser would take you to that specific IdP with the email already filled in.

 

With this knowledge in the back of our minds let us go over a couple of options.

 

Pre-B2C logic with multiple policies

You're not locked to using one single policy for signins so it is easy to create several policies and for instance have a JavaScript snippet send you off to the right one. If you're working with built-in policies this is an easy to implement solution, but with custom policies it doesn't really scale as you potentially end up duplicating the common parts.

 

A more sensible use for this could be to provide switching logic for deciding between signup and signin. In a customer setup I did we have the user type their email address when they click login. In the background a call to the Azure AD Graph API is made to see if the email address exists - if it doesn't exist you're taken to a signup form, and correspondingly you will be sent to the signin form if you have an account. (These are implemented as separate policies.)

 

Pre-B2C logic with domain hints

Instead of implementing one policy pr identity provider you can use the same basic logic as above where you ask for the email address and attach a domain hint to the query string. That way you can have everyone with a "contoso.com" suffix sent to Contoso's Azure AD tenant without ever presenting a choice for the user. For "unknown" suffixes you can skip the hint and present all options.

 

Both of these approaches may very well be all you need to reduce the complexity of the UI. There is however a drawback with both of these options - they require you to implement things outside the B2C sphere. If you just have the one web app that's probably not a big issue. If however you have 10 apps you need to implement it 10 times, and budget for maintenance over time. Not to mention that you're taking dependencies; it could be that the client implementation prevents you from changing the B2C end of things.

 

Can we push it all into B2C and contain it there? Yes, I think we can.

 

"Step zero" HRD logic in B2C with custom policies

The first step in a user journey is usually either the choice of identity provider, or typing in your credentials at a specific provider. But provided we use the login_hint parameter and provide the email address we can have a "step zero" in B2C that processes the adress through an API call and then follow up with a branching exercise within the journey by using conditional matching. (Step zero as in "a non-visible step happening before any UI kicks in".)

 

There are a couple things needed for this to work.

 

Create an Azure Function for doing the backend logic:

run.csx

#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);
    log.Info($"Request: {requestContentAsString}");
   
if (requestContentAsJObject.emailAddress == null)
    {
        log.Info($"Empty request");
        return request.CreateResponse(HttpStatusCode.OK);
    }

    var email = ((string)requestContentAsJObject.emailAddress).ToLower();
    log.Info($"email: {email}");
    char splitter = '@';
    string[] splitEmail = email.Split(splitter);
    var emailSuffix = splitEmail[1];

    //For the "aad" identity provider
    if (email == "bar@foo.com")
    {
        log.Info($"Identity Provider: aad");
        return request.CreateResponse<ResponseContent>(
            HttpStatusCode.OK,
            new ResponseContent
            {
                version = "1.0.0",
                status = (int)HttpStatusCode.OK,
                userMessage = $"Your account is a generic Azure AD account.",
                idp = "aad",
                signInName = email
            },
            new JsonMediaTypeFormatter(),
            "application/json");
    }

    //For B2C local accounts
    if (email == "foo@bar.com")
    {
        log.Info($"Identity Provider: local");
        return request.CreateResponse<ResponseContent>(
            HttpStatusCode.OK,
            new ResponseContent
            {
                version = "1.0.0",
                status = (int)HttpStatusCode.OK,
                userMessage = $"Your account seems to be a local account.",
                idp = "local",
                signInName = email
            },
            new JsonMediaTypeFormatter(),
            "application/json");
    }

    //For Contoso AAD accounts
    if (emailSuffix == "contoso.com")
    {
        log.Info($"Identity Provider: contoso");
        return request.CreateResponse<ResponseContent>(
            HttpStatusCode.OK,
            new ResponseContent
            {
                version = "1.0.0",
                status = (int)HttpStatusCode.OK,
                userMessage = $"Your account belongs to the Contoso Identity Provider",
                idp = "contoso",
                signInName = email
            },
            new JsonMediaTypeFormatter(),
            "application/json");
    }

    else
    {
        log.Info($"Identity Provider: none");
        return request.CreateResponse<BlankContent>(
            HttpStatusCode.OK,
            new BlankContent
            {
                status = (int)HttpStatusCode.OK,
                signInName = email
            },
            new JsonMediaTypeFormatter(),
            "application/json");
    }
}

//Default responses where there is no match
public class BlankContent
{
    public int status { get; set; }
    public string signInName { get; set; }
}

//For responses where there is an IdP matching
public class ResponseContent
{
    public string version { get; set; }
    public int status { get; set; }
    public string userMessage { get; set; }
    public string idp { get; set; }
    public string signInName { get; set; }
}

 

You take in the email, and you can split it to act on the suffix alone, or provide address specific matches. Then you return the identity provider to use back to the B2C policy.

 

It would of course be more likely that you implement database lookups rather than hardcoding like this, but this illustrates the main points nonetheless. Also notice that we pass back a default item with a SigninName claim if there is no hit. This is a trick used for local account hinting later on.

 

Create a ClaimsProvider for doing the call to the backend:

<ClaimsProvider>
     <DisplayName>REST APIs - HRD</DisplayName>
     <TechnicalProfiles>
       <TechnicalProfile Id="HRD_Function">
         <DisplayName>Do an IdP lookup based on email</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://b2c-backend/api/HRD?code=foobar</Item>
           <Item Key="AuthenticationType">None</Item>
           <Item Key="SendClaimsIn">Body</Item>
         </Metadata>
         <InputClaims>
           <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="emailAddress" DefaultValue="{OIDC:LoginHint}" />
         </InputClaims>
         <OutputClaims>
           <OutputClaim ClaimTypeReferenceId="idp" />
         </OutputClaims>
         <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
       </TechnicalProfile>
     </TechnicalProfiles>
   </ClaimsProvider>

 

Create a new user journey with the necessary branching logic (not the complete journey):

<UserJourney Id="HRD_External">
     <OrchestrationSteps>
       <OrchestrationStep Order="1" Type="ClaimsExchange">
         <ClaimsExchanges>
           <ClaimsExchange Id="HRD" TechnicalProfileReferenceId="HRD_Function" />
         </ClaimsExchanges>
       </OrchestrationStep>
       <OrchestrationStep Order="2" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
         <Preconditions>
           <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
             <Value>idp</Value>
             <Action>SkipThisOrchestrationStep</Action>
           </Precondition>
         </Preconditions>
         <ClaimsProviderSelections>
           <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
         </ClaimsProviderSelections>
         <ClaimsExchanges>
           <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
         </ClaimsExchanges>
       </OrchestrationStep>
       <OrchestrationStep Order="3" Type="ClaimsExchange" ContentDefinitionReferenceId="api.signuporsignin">
         <Preconditions>
           <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
             <Value>idp</Value>
             <Value>aad</Value>
             <Action>SkipThisOrchestrationStep</Action>
           </Precondition>
         </Preconditions>
         <ClaimsExchanges>
           <ClaimsExchange Id="AzureADExchange" TechnicalProfileReferenceId="Common-AAD" />
         </ClaimsExchanges>
       </OrchestrationStep>

 

Basically we step into the Azure Function right away, then based on the assumption something was returned from the backend we have preconditions to drive us through the rest of the journey.

 

You might argue that you still need something outside B2C since it requires dynamic creation of the query string instead of a static definition. That's true, but it's still far less than having the actual processing client-side and it doesn't create a dependency the same way either.

 

Internal hinting in B2C with custom policies

It is nice to do the branching inside custom policies. But wouldn't it be even nicer if we were able to move the hinting inside B2C as well?

 

With self-asserted orchestration steps you can basically collect any info you want from the user and send it off to a backend to drive further decisions.

 

This means that we can ask for the email address inside the custom policy, and pass it along to the same Function as in the previous approach.

 

HRD_01.png

 

So, for instance if we type in an address that matches with being an Azure AD account we send the user to that provider directly:

HRD_02.png

 

Notice that we also managed to pass along the email address so no retyping of that necessary either.

 

If we have no match for an existing Identity Provider we can present a more default-like screen with a number of choices. For the sake of choices you can do this two ways as well.

 

If you don't know if they have a local account (you just tried to match up specific IdPs) you can have a signin/signup option for that:

HRD_03_01.png

 

Or if you know that there is no local account there's no need for a signin option, so you can present a signup option instead:

HRD_03_02.png

 

Choosing the email signup option we're taken to a regular sign up screen:

HRD_04.png

 

The user journey for this would look similar to this:

<UserJourney Id="HRD_Internal">
     <OrchestrationSteps>
       <OrchestrationStep Order="1" Type="ClaimsExchange">
         <ClaimsExchanges>
           <ClaimsExchange Id="pre-hrd" TechnicalProfileReferenceId="SelfAsserted-EmailCollect" />
         </ClaimsExchanges>
       </OrchestrationStep>
       <OrchestrationStep Order="2" Type="ClaimsExchange">
         <ClaimsExchanges>
           <ClaimsExchange Id="HRD" TechnicalProfileReferenceId="HRD_Function" />
         </ClaimsExchanges>
       </OrchestrationStep>

 

The important part here is adding a step to collect the email before passing it into the Azure Function.

 

HRD logic in B2C hinting back to JavaScript

It might become messy if you have twenty different providers and you define XML like the above for all of them. And while it is more dynamic than multiple policies it is sort of static as well. There is however yet another option you can go for - manipulating the UI through JavaScript.

 

The JavaScript I refer to here is not any pre-B2C JS your web app might employ, but the JS you can inject in the customized UI you create for B2C. So, while it will obviously need to be developed and maintained outside the XML files it is not a dependency external to B2C. The logic would have B2C doing the same processing as in the previous option, but instead of directing to a specific provider it would return the idp value to JS allowing for hiding and showing providers client side.

 

As already mentioned we can use output claims to collect info, and we can prefill them by using input claims. By hiding the value with JavaScript we can use this as a communication channel of sorts. The user journey in B2C would expose all identity providers, but the JS would tweak which one you would be able to use.

 

Keep in mind that since savvy end-users might be able to manipulate the HTML take care to not expose secrets this way. One thing is creating this for preferring one provider over another, but if you want to make sure that users in Contoso are prevented from using Facebook it might not be enough to just hide that option. But of course, there's nothing preventing you from combining these techniques to have hard and soft restrictions.

 

Since you cannot rely on the value being unmodified coming back from the user you should not store this value backend, so make sure it's not included as a persisted claim.

 

As you might have figured this last technique is usable outside of home realm discovery as well as a generic method for passing info to the JS side and UI tweaking.

 

I will leave this as an exercise for the reader at the moment. Not because it's trivial, (it introduces new challenges), but it would be better covered as a separate post not confusing you more than we've already done here. (No promises yet on a follow-up from my side.)

 

When implementing Azure AD B2C a little creativity goes a long way :)

 

The complete code and policies can be found over at:

https://github.com/ahelland/Identity-CodeSamples-v2/tree/master/aad-b2c-custom_policies-dotnet-core

 

As per usual B2C deployments you can test things directly in the Azure Portal, but if you want to test in a basic web app there's a Docker image you can spin up:

https://hub.docker.com/r/ahelland/aad-b2c-custom_policies-dotnet-core-linux