juunas juunas - 3 months ago 55
C# Question

Azure AD B2C with ASP.NET Core - Unable to go to edit profile

I tried looking for questions relating to this but could not find anything.

I have an ASP.NET Core 1.0 app that uses Azure AD B2C for authentication. Signing and registering as well as signing out work just fine. The problem comes when I try to go edit the user's profile. Here is what my Startup.cs looks like:

namespace AspNetCoreBtoC
{
public class Startup
{
private IConfigurationRoot Configuration { get; }

public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}

// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfiguration>(Configuration);
services.AddMvc();
services.AddAuthentication(
opts => opts.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();

if (env.IsDevelopment())
{
loggerFactory.AddDebug(LogLevel.Debug);
app.UseDeveloperExceptionPage();
}

app.UseStaticFiles();

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticChallenge = false
});

string signUpPolicyId = Configuration["AzureAd:SignUpPolicyId"];
string signUpCallbackPath = Configuration["AzureAd:SignUpCallbackPath"];
app.UseOpenIdConnectAuthentication(CreateOidConnectOptionsForPolicy(signUpPolicyId, false, signUpCallbackPath));

string userProfilePolicyId = Configuration["AzureAd:UserProfilePolicyId"];
string profileCallbackPath = Configuration["AzureAd:ProfileCallbackPath"];
app.UseOpenIdConnectAuthentication(CreateOidConnectOptionsForPolicy(userProfilePolicyId, false, profileCallbackPath));

string signInPolicyId = Configuration["AzureAd:SignInPolicyId"];
string signInCallbackPath = Configuration["AzureAd:SignInCallbackPath"];
app.UseOpenIdConnectAuthentication(CreateOidConnectOptionsForPolicy(signInPolicyId, true, signInCallbackPath));

app.UseMvc(routes =>
{
routes.MapRoute(
name: "Default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

private OpenIdConnectOptions CreateOidConnectOptionsForPolicy(string policyId, bool autoChallenge, string callbackPath)
{
string aadInstance = Configuration["AzureAd:AadInstance"];
string tenant = Configuration["AzureAd:Tenant"];
string clientId = Configuration["AzureAd:ClientId"];
string redirectUri = Configuration["AzureAd:RedirectUri"];

var opts = new OpenIdConnectOptions
{
AuthenticationScheme = policyId,
MetadataAddress = string.Format(aadInstance, tenant, policyId),
ClientId = clientId,
PostLogoutRedirectUri = redirectUri,
ResponseType = "id_token",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
CallbackPath = callbackPath,
AutomaticChallenge = autoChallenge
};

opts.Scope.Add("openid");

return opts;
}
}
}


Here is my AccountController, from where I issue the challenges to the middleware:

namespace AspNetCoreBtoC.Controllers
{
public class AccountController : Controller
{
private readonly IConfiguration config;

public AccountController(IConfiguration config)
{
this.config = config;
}

public IActionResult SignIn()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = "/"
},
config["AzureAd:SignInPolicyId"]);
}

public IActionResult SignUp()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = "/"
},
config["AzureAd:SignUpPolicyId"]);
}

public IActionResult EditProfile()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = "/"
},
config["AzureAd:UserProfilePolicyId"]);
}

public IActionResult SignOut()
{
string returnUrl = Url.Action(
action: nameof(SignedOut),
controller: "Account",
values: null,
protocol: Request.Scheme);
return SignOut(new AuthenticationProperties
{
RedirectUri = returnUrl
},
config["AzureAd:UserProfilePolicyId"],
config["AzureAd:SignUpPolicyId"],
config["AzureAd:SignInPolicyId"],
CookieAuthenticationDefaults.AuthenticationScheme);
}

public IActionResult SignedOut()
{
return View();
}
}
}


I've tried to adapt it from the OWIN example. The problem that I have is that in order to go to edit the profile, I must issue a challenge to the OpenIdConnect middleware that is responsible for that. The problem is that it calls up to the default sign in middleware (Cookies), which realizes the user is authenticated, thus the action must have been something unauthorized, and tries to redirect to /Account/AccessDenied (even though I don't even have anything on that route), instead of going to Azure AD to edit the profile as it should.

Has anyone successfully implemented user profile edit in ASP.NET Core?

Answer

Well, I finally solved it. I wrote a blog article on the setup which includes the solution: https://joonasw.net/view/azure-ad-b2c-with-aspnet-core. The issue was ChallengeBehavior, which must be set to Unauthorized, instead of the default value of Automatic. It wasn't possible to define it with the framework ChallengeResult at the moment, so I made my own:

public class MyChallengeResult : IActionResult { private readonly AuthenticationProperties authenticationProperties; private readonly string[] authenticationSchemes; private readonly ChallengeBehavior challengeBehavior;

    public MyChallengeResult(
        AuthenticationProperties authenticationProperties,
        ChallengeBehavior challengeBehavior,
        string[] authenticationSchemes)
    {
        this.authenticationProperties = authenticationProperties;
        this.challengeBehavior = challengeBehavior;
        this.authenticationSchemes = authenticationSchemes;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        AuthenticationManager authenticationManager =
            context.HttpContext.Authentication;

        foreach (string scheme in authenticationSchemes)
        {
            await authenticationManager.ChallengeAsync(
                scheme,
                authenticationProperties,
                challengeBehavior);
        }
    }
}

Sorry for the name... But this one can be returned from a controller action, and by specifying ChallengeBehavior.Unauthorized, I got everything working as it should.

Comments