Using ADFS as an Identity Provider for Azure AD B2C

Using Azure AD B2C with "regular" Azure AD enabled some new and useful scenarios. Let's take logins further along the same track while we are at it. To not repeat myself I will assume you have at least skimmed through the previous article:

The integration between B2C and AAD is done through the support of the OpenID Connect protocol (building on top of OAuth). In some cases AAD might not be the OpenID Connect provider you want/need - so it should be possible to hook up other providers as well right?

Yes, it should. Being a recognized standard that would be one of the main selling points. I'm not going to show how to integrate B2C with everything out there though, but I thought I could have a crack at bringing good old ADFS to the game.

Why ADFS? Aren't we all about the cloud? Yes, in general I prefer to stick with Azure AD, but there's still times when I want to use my on-prem infrastructure. Perhaps you're using custom MultiFactor Authentication Adapters in ADFS or something. ( It's besides the point really - you have an Identity Provider, and you want to use it 🙂

Note that this only works with ADFS 4.0 (Windows Server 2016). While 2012 R2 supports OAuth, the OpenID Connect support was added in 2016.

But if ADFS 4.0 supports OpenID Connect - why do we go through B2C, could we not skip that? Yes, you can skip B2C, and integrate directly with ADFS. This walkthrough rather ties into taking the integration logic out of your app, and making it a configuration thing server side instead. So, the same way you can integrate directly with Azure AD without B2C, you can use ADFS without B2C. If you have scenarios where you need to support both ADFS and AAD however, B2C makes that easier.

If you want to integrate directly I have a separate guide for that:

So, how do we go about making this work? We need to step through a couple steps in the right order:
- Configure ADFS.
- Create a new IdP config for B2C.
- Modify the B2C app registration to support web apps (if you want to test a web app that is).

If you already have the Windows Universal app from my last article you can reuse that if you like. You shouldn´t need any code changes, and can just hit "run" once you have done the config pieces.

If you want to do something else, like I wanted to, you can for instance base yourself on the same official sample that I did for building a .NET Core Web App:

It´s a fairly clean sample so apart from filling in some specifics related to your B2C tenant it should be easy to get running. (As in, you don´t have to go through ten files, change a bunch of lines, or have a deep understanding of things to be able to compile.)

Let's not get ahead of ourselves though - we need to start on the ADFS Server. I'll assume you have one such server installed, and generally working already. (Man what a hassle I had sorting out the SSL certificates on my new box. But I digress - that´s enough material for a post of its own. I may or may not write that post at a later point in time.)

A thing to be aware of if you have a lab setup with ADFS is that it needs to be available externally. Normally when you develop/test against ADFS you only need a line of sight between your dev box and the ADFS server even if the app taking advantage of it is hosted in Azure. Federation works by issuing redirects so only the client actually talks to ADFS. However, since AAD B2C needs to read the configuration, and handle redirects independently of the client, ADFS needs to be available over the Internet and have a line of sight.

Find "Application Groups" in the ADFS console, right-click, and choose to "Add Application Group". Choose a name, and select "Native application accessing a web API". Hit "Next".

Take note of the client id (you will need this in the B2C portal). The redirect URI should be:{tenantname}

Choose something unique for the identifier.

"Permit everyone" works for now.

Make sure "openid" is checked. Hit Next, review, and finish.

Next you need to head over to your favorite text editor to edit the B2C_1A_TrustFrameworkExtensions.xml file. (If you're following the naming from the starter pack used for setting up custom policies.) I basically copied the AAD ClaimsProvider, and made some minor edits.

    <DisplayName>Login using ADFS</DisplayName>
    <TechnicalProfile Id="ADFSProfile">
        <DisplayName>Contoso ADFS</DisplayName>
        <Description>Login with your Contoso account</Description>
        <Protocol Name="OpenIdConnect" />
        <Item Key="METADATA"></Item>
        <Item Key="ProviderName"></Item>
        <Item Key="client_id">adfs-client-id</Item>
        <Item Key="IdTokenAudience">adfs-client-id</Item>
        <Item Key="response_types">id_token</Item>
        <Item Key="UsePolicyInRedirectUri">false</Item>
        <OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="sub" />
        <OutputClaim ClaimTypeReferenceId="tenantId" PartnerClaimType="tid" />
        <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
        <OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
        <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
        <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="contosoAuthentication" />
        <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="ContosoADFS" />
        <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" />
        <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName" />
        <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" />
        <OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId" />
        <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />

Things to note:
- Make sure "METADATA" and "ProviderName" point to your ADFS instance. And make sure it's a valid SSL cert and all those things.
- "client_id" and "IdTokenAudience" are identical and contains the value from the app you created on the ADFS server.
- "socialIdpUserId" needs to be changed to a value you get from ADFS as a claim. I used sub as the id, but you could use upn if you like. Main thing; it needs to be a unique value that doesn't change. (Which technically is a recommendation to not use the upn since that might change.)

You should also update your user journeys to reference this new claims provider:

<OrchestrationStep Order="2" Type="ClaimsExchange">
    <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
    <!--<ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH" />-->
    <!--<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />-->
    <ClaimsExchange Id="ContosoExchange" TechnicalProfileReferenceId="AADProfile" />
    <ClaimsExchange Id="ADFSExchange" TechnicalProfileReferenceId="ADFSProfile" />

Then you need to make a minor edit on the B2C portal. Locate the B2C app (which I have called "B2C SuperPlus" in my tenant):

Switch "Web App / Web API" to "Yes", and add a reply URL for your app:

Note - this is not related to the setting on the ADFS server. This is between B2C and your app.

Back in our web app we change things accordingly:

Yes, I am running it on a Mac just for the fun of it, but of course it´s the same if you happen to use Windows (where I usually do my coding) 🙂

Do remember that the client id here is the one for the B2C registration; it is not related to the ids you created on the ADFS server.

I cleaned up the selection of Identity Providers, so running the app you can see I now get to choose between my AAD or my ADFS instance. (B2C without any social IdPs - oh, the irony.) Since I am running AAD Connect in my setup the credentials are the same, but this is of no concern to the app. (Btw: this means that you can of course run this kind of "dual identity stack" without issues.)

Some closing remarks. If you assemble things incorrectly, and it fails somewhere between the Idp and B2C you may see generic error messages that aren´t really all that helpful.
I highly recommend checking out for the details on how to enable Application Insights to collect the logs. It´s not like the error will reveal itself in a matter of seconds, but diving into the logs might give a hint to something you´ve missed. (Took me a little tinkering to sort out my misconfigured claim this way, but I eventually realized what I was doing wrong.)

Comments (8)

  1. tharpster says:

    Great article! Can anyone explain why B2C needs line of site to ADFS? This article states I can’t have ADFS protected inside a corporate network, but I am not sure why this is the case, as is also stated SAML federation only requires the browser to have line of sight with ADFS. My assumption was that B2C would redirect the browser/user to ADFS, which would redirect back to B2C, which would redirect back to my application. Is this possible without ADFS being available externally?

    1. Well, technically OpenID Connect does not require line of sight as such. However B2C uses the metadata url (https://xyz/.well-known/openid-configuration) server side , and is unable to parse this if ADFS cannot be reached. Would it work if one could upload the metadata? Not sure. I tried loading the metadata manually from an ADFS server once, and it was a real hassle in .NET. So, the design is certainly easier this way.
      I have seen setups where companies don’t expose ADFS externally, but I’m guessing that quite a lot of deployments are external-facing.

  2. zvita says:

    Would a ADFS-federated Azure AD domain work as IdP for Azure B2C? I’ve been trying for days now but all documents just asume we all know how to use Visual Studio and that’s where I get lost. 🙁 When my domain is input Azure redirects to the local servers for authentication but I’ve noticed websites that can use Azure AD as IdP fail without much as to why.

    1. In theory it should work, but I have not tried it myself. B2C should send you to AAD, which in turn should send you to ADFS, and then you’ll be returned accordingly.

      There are a few things I can see potentially breaking stuff though:
      – Is the integration to AAD through the common or tenant-specific endpoint? V1 or V2 endpoint?
      – Could there be something with the claims mapping? AAD makes some translations under the cover, so make sure these fit into B2C as well.

      Are there any error messages somewhere along the way when attempting this? (Cryptic or not.)

      1. zvita says:

        Thanks for getting back 🙂 TBH I’m perfectly ignorant on all of this though I’m making progress, I thought myself how to deploy apps and several missing pieces I wasn’t getting in the docs, and yeah, I figured they’d be some gotcha with AAD so I resigned myself I just need to suck it up and learn it. I’m hoping it will be like it was with SAML, I didn’t get it either but once my first federation worked it all clicked and now I want to federate everything. It would be much easier if I knew the slightest bit of code, though.

        I don’t have any messages to show because I haven’t made any connections yet, finding info about deploying apps, plus several little side trips I took kept me busy, I was just know about to try the first one but following the docs for implementing OpenID Connect in ADFS, no Azure whatsoever, which seemed very very clear and I got it working locally but, I remembered I bookmarked your article and gave it another shot and sort of used the working set up and broke it again. 😂 I more or less of memorized it by now though, so hopefully in a few mins I finally have a userbase on B2C. Thanks again for sharing, it really helped me, specially with all the links.

      2. zvita says:

        I started all over again, it turns out there was some sort of and error on the federation between AAD and ADFS, hence AAD-AADB2C would keep failing adding the OpenID Connect AAD provider at the very last moment when I click on Save–nice. 😒 I got better sense of things and it was just like I mentioned before; once I got it working it’d all click!

        Your guide, Microsoft’s, those on GitHub are very informative, maybe a little bit too much; they all tell the new user/learner, as it is in most of our cases to deploy a client to test it out, I assume it’s because on itself, the bits necessary for B2C to be considered as successfully setup do nothing on their own, they need something to work with and demonstrate proof of concept, that’s fine; except no one remembers to pause and mention just that, though it’s implied very vaguely raising many more questions, for instance, “what do I need to host a website (web app) to store users, don’t B2C is supposed to do it? what’s then point then” stuff like that, because while it may be implied that the web apps being deployed are test benches, it is much strongly implied those are actual functioning parts on which B2C will depend. Before going on to get the files for the web app a nice, simple “next you’ll need a web app to test your IdP — IF — you don’t have one you can may try these sample cod…” mention leaves no doubt what each piece does. By the way, it does work with federated AAD, users had to be synced but works. I actually came to tell you that almost didn’t make it ’cause I couldn’t find the link! 🤤

        Have a great day!

        1. It’s a challenge writing both blogs posts and documentation to sort out some of the issues you mention, and I’m very aware of them. Which level should I target – beginner or pro? And how extensive should explanations be – people generally don’t read posts that are too long.

          Sometimes this results in implying that readers already have some components in place, or know how to fix it themselves in a short time. And sometimes we miss the mark making these assumptions.

          This is one of those things I personally liked very much about tech books in the past. You had plenty of time and space to cover everything if you wanted, or you could target audiences by adding prerequisites. Unfortunately books are less dynamic and can’t keep up with cloud speeds.

          For testing B2C I actually do a lot of it just by executing the policies in the browser and setting as the reply url. That way I can test the general login procedure without involving any code on my part. (Of course I need web app running at some point for further testing.)

          Anyways – great that you got things working and can verify the use case itself works!

Skip to main content