asp.net

Connecting SharePoint to Azure AD B2C

September 8, 2016 Coding 4 comments , , ,

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, "/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.

External Auth in ASP.Net MVC/SPA/Web API apps

July 31, 2014 Coding 3 comments ,

Out of the box, Visual Studio 2013 comes with a number of templates for ASP.Net to cover most scenarios. The issue is that if you want registered users, you have a choice between Individual User Accounts, Organizational Accounts or Windows Authentication.

VS 2013 ASP.Net Options

While “Individual User Accounts” sounds like the only option for apps that aren’t using Azure AD or Windows Auth, the trouble is that it brings in all of ASP.Net Identity, which includes a local user database. If you look at the description above, it seems like this is the option to use for external/social logins, but as we’ll discuss shortly, it’s a red herring.

There are a few reasons why it’s not a good idea to mix your user database with your main app:

  • Single Responsibility Principle. This is the “S” in SOLID, and it applies to your application/service as a whole, not just your classes. Your application should be great at what it does. Your services should be granular and each do a single thing. Your app is the best thing since sliced bread. What it doesn’t need, is to manage users.
  • Security is hard. If you look at the template code generated by the ASP.Net wizards for “Individual User Accounts,” you’ll see tons of code in the AccountController class. Go ahead, take a look, I’ll wait. Back yet? That code is now on you to understand and work with as after creation, it’s no longer a library. As you go about adjusting the templates, and views, you need to make sure you don’t accidentally introduce any issues. And you will need to be touching that code as the templates are not complete; they’re a getting-started point.
  • Higher Risk. Even though the password field in ASP.Net Identity is salted and hashed, there’s still a lot of juicy information in your users table that a 1337 [email protected] would love to get their hands on. You don’t want that 3am phone call.

Now that you’re hopefully thoroughly convinced that you do not want to implement your own authentication, you’re wondering what can you do now? The answer is to use an external authentication provider. Facebook is one, Microsoft Account another, and Google yet another. There’s no shortage of external authentication providers these days. If you’re thinking that “Individual User Accounts” with ASP.Net Identity does this for you (it’s in the description and template code!), don’t lose sight of the fact that it’s really creating new identities in your app and simply linking external logins. See Brock Allen’s detailed description on how it all works. It’s the same core issue where your app is now managing identities.

Returning to that initial ASP.Net Authentication options dialog, there’s one option that’s conspicuously absent: External Identity Provider. This is one option that really needs to be present in the dialog—a set of templates that are geared around validating external claims that aren’t part of Azure AD.

For Web API apps validating a Bearer token, this comes down to a single line of code (with Thinktecture’s OWIN component):

app.UseJsonWebToken({issuerId}, {audience}, {appSecret});

With that one line, your Web API can rely on true external security.

To be clear, relying an an external provider does not mean that you need to give up the idea of having “local users,” or multiple social logins like Facebook either. If you don’t want to be tied to a single provider, you can use a service like Auth0 or an configurable identity app like Identity Server. Either of those options provides an appropriate architectural separation of concerns and lets the experts handle the authentication. Your app becomes a Relying Party to the external authentication service, receiving, and validating, a set of claims (such as an email address or unique user id in their system).

The key difference between ASP.Net Identity’s assumptions and templates and using an external provider is who owns the claims. ASP.Net Identity translates external claims into its own identity system and issues its own new set of claims and tokens. My recommendation is that this extra layer is unnecessary and your application should use externally provided claims primarily.

There’s one area that I haven’t covered yet: what if you have your Web API and MVC content (even a SPA) in the same site? You’d like your login mechanism to work for both your web UI and your API. This is an area that has caused much confusion in current incarnations as Web API and MVC each do authentication a bit differently. For web browsers, you need some form of cookie auth while still having bearer tokens available for your API. This is exactly the situation I was in where I fell into the ASP.Net Identity trap. I’ll talk more about what happened and how I solved it next time.

To sum up, if you’re not sure whether to use ASP.Net Identity, this flow chat might help:

Special thanks to Barry Dorrans for reviewing this content prior to release.