Connecting SharePoint to Azure AD B2C

Overview

This post will describe how to use Azure AD B2C as an authentication mechanism for SharePoint on-prem/IaaS sites. It assumes a working knowledge of identity and authentication protocols, WS-Federation (WsFed) and OpenID Connect (OIDC). If you need a refresher on those, there are some great resources out there, including Vittorio Bertocci’s awesome book.

Background

Azure AD B2C is a hyper-scalable standards-based authentication and user storage mechanism typically aimed at consumer or customer scenarios. It is a separate product from “regular” Azure AD. Whereas “regular” Azure AD is normally meant to house identities for a single organization, B2C is designed to host identities of external users. In my opinion, it’s the best alternative to writing your own authentication mechanism (which no one should ever do!)

For one client, we had a scenario where we needed to enable external users to access specific site collections within SharePoint. Azure AD wasn’t a good fit, even with the B2B functionality, as we needed to collect additional information during user sign-up. Out-of-the-box, B2C doesn’t yet support WsFed or SAML 1.1 and SharePoint doesn’t support OpenID Connect. This leaves us needing a tool that can bridge B2C to SharePoint by acting as an OIDC relying party (RP) to B2C and a WsFed Identity Provider (IdP) to SharePoint.

The Solution

Fortunately, the identity guru’s Dominick Baier and Brock Allen created just such a tool with IdentityServer 3. From the docs:

IdentityServer is a framework and a hostable component that allows implementing single sign-on and access control for modern web applications and APIs using protocols like OpenID Connect and OAuth2.

IdentityServer has plugins to support additional functionality, like acting as a WsFed IdP. This means we can use IdentityServer as a bridge from OIDC to WsFed. We’ll register an application in B2C for IdentityServer and then create an entry in IdentityServer for SharePoint.

Here’s a diagram of the pieces:
Diagram

While you can use IdentityServer to act as an IdP to multiple clients, in the model we used, we considered IdentityServer as “part of SharePoint.” That is, SharePoint is the only client and in B2C, the application entry visible is called “SharePoint.” I mention this because B2C allows applications to choose different policies/flows for sign up/sign in, password reset, and more. In our solution, we’ve configured IdentityServer to use a particular set of policies that meet SharePoint’s needs — it many not meet the needs of other applications.

Diving deep

As mentioned, IdentityServer isn’t so much a “drop in product,” but rather, it’s a framework that needs customization. The rest of this post will look at how we customized and configured B2C, IdentityServer and SharePoint to enable the end-to-end flow.

B2C

Let’s start with B2C. As far as B2C is concerned, we register a new web application and create a couple of policies: sign-up/sign-in and password reset. When you register the application, enter the redirect uri’s you’ll need for IdentityServer (localhost and/or your real url.) You don’t need a client secret for these flows.

IdentityServer

Follow the IdentityServer getting started guide to create a blank ASP.NET 4.6 MVC site and install/configure IdentityServer. ASP.NET Core is not yet supported on CoreCLR as .NET Core doesn’t yet have the XML cryptography libraries needed for WsFed (that support will come as part of .NET Standard 2.0.) After installing the IdentityServer3 NuGet, you’ll need to install the WsFed plugin NuGet.

The key here is that we don’t need a local user store as IdentityServer won’t be acting as the user database. We just need to configure B2C as an identity provider and the WsFed plugin to act as an IdP. IdentityServer won’t maintain any state and is simply a pass-through, validating JWT’s and issuing SAML tokens.

Below, I’ll explain some of the core snippets; the full set of files are available here.

Inventory

There are several areas of IdentityServer that need to either be configured or have custom code added:

  • Identity Provider B2C via the standard OIDC OWIN middleware
  • WS-Federation plugin IdentityServer plug for WsFed
  • Relying parties An entry or two for your WsFed/SAML client (SharePoint or a test app configured with WsFed auth)
  • User Service IdentityServer component for mapping external auth to users

Identity Provider

We need to configure B2C as an OIDC middleware to IdentityServer. Due to the way B2C works, we need some additional code to handle the different policies — it’s not enough to configure a single OIDC endpoint. For normal flows, it’ll default to the policy specified in “SignInPolicyId”. Where it gets tricky is in handling password reset.

First, let’s look at the normal “happy path” flow, where a user either sign’s up or sign’s in. Here’s what the flow looks like:
Sign in

In B2C, password reset is a separate policy and thus requires a specific call to the /authorize endpoint specifying the password reset policy to use. If you use the “combined sign up/sign in” policy, which is recommended as it’s the most styleable, it provides a link button for “password reset”. What this does, however, is return a specific error code to the app that started the sign up flow. It’s up to the app to start the password reset flow. Then, once the password reset flow is complete, despite appearing to be authenticated (as defined by having had a signed JWT returned), B2C’s SSO mechanisms won’t consider the user signed in. You’ll notice this if you try to use a profile edit flow or any other flow where SSO should have signed in the user w/o additional prompting. The guidance from the B2C team here is that after the password reset flow completes, an app should immediately trigger the sign in flow again. Logically, this makes sense, as a user started the reset password flow from the sign in screen, once the password is reset, they should logically resume there to actually sign in.

Sign in with password reset

Implementing this all with IdentityServer requires a little bit of extra code. Unfortunately, with IdentityServer, we cannot simply add individual OIDC middleware instances for each endpoint as we would in a normal web app because IdentityServer will see them as different providers and present an identity provider selection screen. To avoid this, we are only configuring a single identity provider and passing the policy as an authentication parameter. The B2C samples provide a PolicyConfigurationManager class that can retrieve and cache the OIDC metadata for each of the policies (sign-up/sign-in and password reset).

Here’s an example from Startup.Auth.B2C.cs:

ConfigurationManager = new PolicyConfigurationManager(
    string.Format(CultureInfo.InvariantCulture, B2CAadInstance, B2CTenant, "https://orencodesblog.azureedge.net/v2.0", OIDCMetadataSuffix), 
    new[] {SignUpPolicyId, ResetPasswordPolicyId}),

The main work in getting IdentityServer to handle the B2C flows are in handling the OpenID Connect Event’s RedirectToIdentityProvider, AuthenticationFailed, and SecurityTokenValidated. By handling these three, we can bounce between the flows.

In the Startup.Auth.B2C.cs file, the OnRedirectToIdentityProvider event handler looks for the policy authentication parameter and ensures the correct /authorize endpoint is used. As IdentityServer handles the initial auth call, we cannot specify a policy parameter, so we assume it’s a sign-in. IdentityServer tracks some state for the sign in request, and we’ll need access to it in case the user needs to do a password reset later, so we store it in a short-lived, encrypted, session cookie.

Once the B2C flow comes back, we need to handle both the failed and validated events. If failed, we look for the specific error codes and take appropriate action. If success, we check if it’s from a password reset and then bounce back to the sign in to complete the journey.

WS-Federation plugin

Configuring IdentitySever to act as a WS-Federation IdP is fairly simple: install the plugin package and provide the plugin configuration in Startup.cs. As an aside, don’t forget to either provide your own certificate or alter the logic to pull the cert from somewhere else!

The main WsFed configuration is a list of Relying Parties, seen in RelyingParties.cs. I’ve hard-coded it, but you can generate this data however you see fit.

Relying parties

Within the Relying Party configuration, you can specify the required WsFed parameters, including Realm, ReplyUrl and PostLogoutRedirectUris. The final thing you need is a map of OIDC claims to SAML claim types returned.

User Service

The User Service is what IdentityServer uses to match external claims to internal identities. For our use, we don’t have any internal identities and we simply pass the claims through as you can see in AadUserService.cs. The main thing we do is to extract a few specific claims and tell IdentityServer to use those for name, subject, issuer and authentication method.

WsFed Client (or SharePoint)

Adding a WsFed client should be faily easy at this point. Configure the realm and reply url’s as required and point to the metadata address. For IdentityServer, this is https://localhost:44352/wsfed/metadata by default (or whatever your hostname is.)

ASP.NET 4.6

I find it useful to have a basic ASP.NET MVC site I can use for testing that authenticates and prints out the claims — helps isolate me from difficult SharePoint issues.

With ASP.NET MVC 4.6, add the Microsoft.Owin.Security.WsFederation NuGet package and use this in your Startup class where realm is the configured realm and adfsMetadata is the IdentityServer metadata endpoint:

public void ConfigureAuth(IAppBuilder app)
{
    app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

    app.UseCookieAuthentication(new CookieAuthenticationOptions());

    app.UseWsFederationAuthentication(
        new WsFederationAuthenticationOptions
        {
            Wtrealm = realm,
            MetadataAddress = adfsMetadata
        });
}

SharePoint

I will readily confess that I am not a SharePoint expert. I’ll happily leave that to other’s like Bob German, a colleague and SharePoint MVP. From Bob:

The Microsoft documentation is fine, but is oriented toward a connection with Active Directory via AD FS, so it includes claims attributes such as the SID value, which won’t exist in this scenario. The only real claims SharePoint needs are email address, first name, and last name. Any role claims passed in are available for setting permissions in SharePoint. Follow the relevant portions of the documentation, but only map the claims that make sense.

For example,

$emailClaimMap = New-SPClaimTypeMapping -IncomingClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" -IncomingClaimTypeDisplayName "EmailAddress" -SameAsIncoming
$firstNameClaimMap = New-SPClaimTypeMapping -IncomingClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" -IncomingClaimTypeDisplayName "FirstName" -SameAsIncoming
$lastNameClaimMap = New-SPClaimTypeMapping -IncomingClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" -IncomingClaimTypeDisplayName "LastName" -SameAsIncoming
$roleClaimMap = New-SPClaimTypeMapping -IncomingClaimType "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" -IncomingClaimTypeDisplayName "Role" -SameAsIncoming

New-SPTrustedIdentityTokenIssuer -Name <somename> -Description <somedescription> -realm <realmname> -ImportTrustCertificate <token signing cert> -ClaimsMappings $emailClaimMap,$roleClaimMap,$firstNameClaimMap,$lastNameClaimMap -IdentifierClaim $emailClaimMap.InputClaimType

You can pass in additional claims attributes and SharePoint’s STS will pass them along to you, but you can only access them server-side via Thread.CurrentPrincipal; for example,

IClaimsPrincipal claimsPrincipal = Thread.CurrentPrincipal as IClaimsPrincipal;
If (claimsPrincipal != null)
{
    IClaimsIdentity claimsIdentity = (IClaimsIdentity)claimsPrincipal.Identity;
    foreach (Claim c in claimsIdentity.Claims)
    {
        // Do something
    }
}

With this scenario, you can assign permissions based on an individual user using the email claim, or based on a role using the role claim. However SharePoint’s people picker isn’t especially helpful in this case. Since it has no way to look up and resolve the claims attribute value, it will let users type anything they want. Type something and then hover over the people picker; you’ll see a list of claims. Select the Role claim to grant permission based on a role, or the Email claim to grant permission to an individual user based on their email address.

SharePoint does not use WsFed metadata, so you need to provide the signing certificate’s public key directly and specify the WsFed signin url. For the scenario here, that’s https://localhost:44352/wsfed

Conclusion

While not without its challenges, it is possible to use B2C with a system that only knows WsFed. One thing I have not yet done is implement a profile edit flow. I need to give that more thought around how that’d work and interact. I’m open to ideas if you have them and I’ll blog a follow-up once that’s done.