Azure AD B2C Solved…for now

After my last post, I got an answer back on Stack Overflow which fixed the basic problem I was having with getting the Microsoft Account identity provider to work. It turns out I had misconfigured the redirect uri in the Microsoft Application portal.

But the more interesting question is how did I end up misconfiguring it? While I now know a lot more about how Azure AD B2C works, I definitely didn’t know enough when I set up my simple AspNet Core website to choose a different configuration strategy. Put another way, I was following directions from somewhere in Microsoft’s online world…so how did I end up with a non-functional identity provider?

The answer, I think, is that there are two different sets of online documentation, both of which describe configuring identity providers in very similar ways, but each of which relies on different infrastructure to check identities. These are OpenID and Azure AD B2C.

You configure them in the site’s Startup class the same way:

public class Startup
{
    public Startup( IConfiguration configuration )
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices( IServiceCollection services )
    {
        services.Configure<CookiePolicyOptions>( options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        } );

        services.AddAuthentication( AzureADB2CDefaults.AuthenticationScheme )
            .AddAzureADB2C( options => Configuration.Bind( "AzureADB2C", options ) );

        services.AddAuthorization( options =>
        {
            options.AddPolicy( "Registered",
                policy => policy.RequireAuthenticatedUser().RequireClaim( "Registered-User" ) );
            options.AddPolicy( "Unregistered", policy => policy.RequireAuthenticatedUser() );
        } );

        services.AddDbContext<RideMonitorContext>( options =>
            options.UseSqlServer( Configuration.GetConnectionString( "DefaultConnection" ) ) );

        services.AddMvc().SetCompatibilityVersion( CompatibilityVersion.Version_2_1 );
    }

    private void ConfirmRegisteredUser( ClaimsPrincipal principal )
    {
        var registered = principal.Identity.Name.Equals( "mark@arcabama.com", StringComparison.OrdinalIgnoreCase );
        var registeredClaim = new Claim( "Registered-User", registered.ToString(), ClaimValueTypes.Boolean );
        ( principal.Identity as ClaimsIdentity )?.AddClaim( registeredClaim );
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure( IApplicationBuilder app, IHostingEnvironment env )
    {
        if( env.IsDevelopment() )
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler( "/Error" );
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles(); 
        
        // always being secure causes a correlation error, probably having to do with cookies
        // being sent, or not being sent, when an http request hits
        //app.UseCookiePolicy(new CookiePolicyOptions { Secure = CookieSecurePolicy.Always });
        app.UseCookiePolicy();

        app.UseAuthentication();
        app.UseMiddleware<RouteAnonymous>();

        app.UseMvc();
    }
}

As I recall, about the only difference between the two is in the call to services.AddAuthentication(), and involves what authentication scheme you’re relying on (ignore the ConfirmRegisteredUser() method; it’s something unique to my site, but not germane to this discussion).

The real differences are in the configuration parameters set in the appsettings.json file. Here’s what ended up working with Azure AD B2C:

{
  "AzureADB2C": {
    "Instance": "https://ridemonitor.b2clogin.com",
    "ClientId": redacted,
    "Domain": "ridemonitor.onmicrosoft.com",
    "SignUpSignInPolicyId": "b2c_1_SignUpIn",
    "ResetPasswordPolicyId": "b2c_1_PWReset",
    "EditProfilePolicyId": "b2c_1_ProfileEditing"
  },
  "Logging": { "LogLevel": { "Default": "Warning" } },
  "AllowedHosts": "*"
} 

The OpenID version of this file has a lot of additional entries, relating to redirects, callbacks, client secrets, etc. All of that is subsumed within how you configure Azure AD B2C through various web portals (e.g., you must have a client secret corresponding to that client ID, but it’s part of the Azure AD B2C configuration, not your app configuration).

In hindsight, the presence of redirects in the appsettings.json file should’ve been a tipoff that something odd was going on. Why would they need to be both defined in the app and defined in the various online portals (Azure AD B2C, Microsoft Application, Google Credentials) the app was using? The answer is that they don’t need to be in two places; only the stuff configured through the online portals matter, except for the basic things shown in that reduced appsettings.json file.

In the end, conflating the directions for OpenID and Azure AD B2C is what caused my problems. Azure AD B2C is, I think, a particular implementation of OpenID, which is why its configuration looks similar. But where OpenID, implemented in a website, requires redirects back to that website — it’s the website, in that case, which is doing the actual authentication — Azure AD B2C needs to have its redirects point back to someplace in “Azure space”, i.e., ridemonitor.b2clogin.com.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Archives
Categories