Azure B2C Migration from Preview to Production tenant Leads to successful login without the Iprincipal being passed back - azure

Guys I am not including code ATM because this seems to be a Azure B2C setup question not a coding question. If needed I will upload it but the code in question is quite a bit to read thru.
Q: Has anyone successfully migrated from a Azure B2C "Preview" Tenant to a "Production" Tenant?
We had a functioning website using the B2C "Preview" Tenant and Microsoft informed us that we needed to create a "Production" Tenant now that it had been released. We deleted the "Preview" tenant and created back the "Production" tenant with the same name but when we did that we lost our fallback to the working "Preview" Tenant. The New "Production" tenant failed to create with the b2c_extensions-app that we had in the "Preview" Tenant which we believe caused it to be not functional. So we created a second "Production" Tenant with a new name and that did create with the b2c-extensions-apps and we proceeded to change the web apps settings to point to the new name. Now when we signup the user gets created in the new B2C AD but when Microsoft comes back to our return URL the returned IPrincipal has no claims and the User.Identity.IsAuthenticated is false. How can a user get created in the B2C and return you get a User.Identity.IsAuthenticated=false?
Additional Info: the ID_Token is on the authresp. It is looking like MVC is not decrypting the encrypted token and creating the Iprincipal User. We are currently using package System.IdentityModel.Tokens.Jwt version 4.0.2.206221351. Could it be that the new Production version of B2C AD only works with System.IdentityModel.Tokens.Jwt version 5.0.0?

Well it took a month of Microsoft working on it to get a workaround to this and get our website back up and working. Bottom line: Below is the takeaways from this experience.
1) Do not delete your Preview B2C until you are sure your Production one is 100% working with your website.
2) When creating your production B2C do not use the same name as you did on your Preview B2C. ( this is a known bug to Microsoft. Good to know after the fact, Right?)
3) Do not use the same names for your Sign-in, Sign-up, Password-Reset, or Profile-editing policies. ( this is critical.)
You must change the port number you use for testing locally in the Azure B2C Application Reply Url, your web.config ReturnURL variable, and in the projects properties setting. (This was also critical for us.)
5). Microsoft had us change the calls to OpenIdConnect in the following places:
a) the ida:AadInstance in the web.config to
<add key="ida:AadInstance" value="https://login.microsoftonline.com/{0}/v2.0/.well-known/openid-configuration?p={1}"/>
b) The modified code in App_Start/Startup.Auth.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using System.Threading.Tasks;
using Microsoft.Owin.Security.Notifications;
using Microsoft.IdentityModel.Protocols;
using System.Web.Mvc;
using System.Configuration;
using System.IdentityModel.Tokens;
using System.Threading;
using System.Globalization;
using Microsoft.Owin;
namespace WebSite
{
public partial class Startup
{
// The ACR claim is used to indicate which policy was executed
public const string AcrClaimType = "http://schemas.microsoft.com/claims/authnclassreference";
// App config settings
public static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
public static string aadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
public static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
public static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
// B2C policy identifiers
public static string SignUpPolicyId = ConfigurationManager.AppSettings["ida:SignUpPolicyId"];
public static string SignInPolicyId = ConfigurationManager.AppSettings["ida:SignInPolicyId"];
public static string ProfilePolicyId = ConfigurationManager.AppSettings["ida:UserProfilePolicyId"];
public static string ChangePasswordPolicyId = ConfigurationManager.AppSettings["ida:ChangePasswordPolicyId"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Configure OpenID Connect middleware for each policy
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignUpPolicyId));
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInPolicyId));
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(ProfilePolicyId));
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(ChangePasswordPolicyId));
}
// Used for avoiding yellow-screen-of-death
private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
MetadataAddress = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant, policy),
AuthenticationType = policy,
ClientId = clientId,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = AuthenticationFailed
},
Scope = "openid",
ResponseType = "id_token",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
SaveSigninToken = true //important to save the token in boostrapcontext
},
ProtocolValidator = new OpenIdConnectProtocolValidator { RequireNonce = false }
};
}
}
}
c) The modified code in Controllers/AccountController.cs (see attached code)
using Microsoft.Owin.Security;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Microsoft.Owin.Security.OpenIdConnect;
using Microsoft.Owin.Security.Cookies;
using System.Security.Claims;
using EMC_Portal_Web.Services.DataAccess;
using EMC_Portal_Web;
namespace WebSite.Controllers
{
public class AccountController : Controller
{
public void SignIn()
{
// To execute a policy, you simply need to trigger an OWIN challenge.
// You can indicate which policy to use by adding it to the AuthenticationProperties using the PolicyKey provided.
try
{
if (!Request.IsAuthenticated)
{
// To execute a policy, you simply need to trigger an OWIN challenge.
// You can indicate which policy to use by specifying the policy id as the AuthenticationType
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = Startup.redirectUri }, Startup.SignInPolicyId);
}
}
catch (Exception ex)
{
Trace.TraceError("Error Message: " + ex.Message + " Stack: " + ex.StackTrace);
}
}
public void SignUp()
{
try
{
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = Startup.redirectUri }, Startup.SignUpPolicyId);
}
}
catch (Exception ex)
{
Trace.TraceError("Error Message: " + ex.Message + " Stack: " + ex.StackTrace);
}
}
public new void Profile()
{
try
{
if (Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = Startup.redirectUri }, Startup.ProfilePolicyId);
}
}
catch (Exception ex)
{
Trace.TraceError("Error Message: " + ex.Message + " Stack: " + ex.StackTrace);
}
}
public void ChangePassword()
{
try
{
if (Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = Startup.redirectUri }, Startup.ChangePasswordPolicyId);
}
}
catch (Exception ex)
{
Trace.TraceError("Error Message: " + ex.Message + " Stack: " + ex.StackTrace);
}
}
public ActionResult SignOut()
{
try
{
if (Request.IsAuthenticated)
{
IEnumerable<AuthenticationDescription> authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
}
return Redirect(System.Web.HttpContext.Current.Application["Index"].ToString());
}
catch (Exception ex)
{
Trace.TraceError("Error Message: " + ex.Message + " Stack: " + ex.StackTrace);
return Redirect(System.Web.HttpContext.Current.Application["Home"].ToString());
}
}
}
}

Related

Usage of the /common endpoint is not supported for such applications created after '10/15/2018' issue

Similar issue here. I have checked the answer and try to implement all the possible forms of link in my startup.cs class with the following code:
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithTenantId(tenantId)
.WithClientSecret(appSecret)
.WithAuthority(Authority) // Authority contains the link as mentioned in the page(link attached above)
.Build();
I still get the similar error:
"OpenIdConnectMessage.Error was not null, indicating an error. Error: 'invalid_request'. Error_Description (may be empty): 'AADSTS50194: Application 'xxx-xxx-xxx-xxx-xxxx'(ASPNET-Quickstart) is not configured as a multi-tenant application. Usage of the /common endpoint is not supported for such applications created after '10/15/2018'. Use a tenant-specific endpoint or configure the application to be multi-tenant.
Trace ID: xxx-xxx-xxx-xxx-xxxx
Correlation ID: xxx-xxx-xxx-xxx-xxxx
Timestamp: 2022-06-11 05:33:24Z'. Error_Uri (may be empty): 'error_uri is null'."
The combination of links I have used in variable Authority are the following: "https://login.microsoftonline.com/MY_TENANT_NAME" and "https://login.microsoftonline.com/MY_TENANT_ID"
I am being redirect to login page but after entering credentials OnAuthenticationFailedAsync method is being executed. This is the code of my startup class:
[assembly: OwinStartup(typeof(Web.Startup))]
namespace Web
{
public partial class Startup
{
// Load configuration settings from PrivateSettings.config
private static string appId = ConfigurationManager.AppSettings["ida:AppId"];
private static string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private static string graphScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
private static string tenantId = ConfigurationManager.AppSettings["ida:tenantId"];
private static string aadInstance = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:AADInstance"]);
public static string Authority = "https://graph.microsoft.com/"+ tenantId;
string graphResourceId = "https://graph.microsoft.com/";
public void Configuration(IAppBuilder app)
{
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/common/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
// For demo purposes only, see below
ValidateIssuer = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
private static Task OnAuthenticationFailedAsync(AuthenticationFailedNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
string redirect = $"/Home/Error?message={notification.Exception.Message}";
if (notification.ProtocolMessage != null && !string.IsNullOrEmpty(notification.ProtocolMessage.ErrorDescription))
{
redirect += $"&debug={notification.ProtocolMessage.ErrorDescription}";
}
notification.Response.Redirect(redirect);
return Task.FromResult(0);
}
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithTenantId(tenantId)
.WithClientSecret(appSecret)
.WithAuthority(Authority)
.Build();
string email = string.Empty;
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
email = await GraphHelper.GetUserDetailsAsync(result.AccessToken);
}
catch (MsalException ex)
{
System.Diagnostics.Trace.TraceError(ex.Message);
}
notification.HandleResponse();
notification.Response.Redirect($"/Account/SignInAzure?email={email}");
}
private static string EnsureTrailingSlash(string value)
{
if (value == null)
{
value = string.Empty;
}
if (!value.EndsWith("/", StringComparison.Ordinal))
{
return value + "/";
}
return value;
}
}
}
My application is for single tenant so please don't suggest me to change the setting and make it for multi-tenant.
Please check below points:
After trying to change it to specific tenant i.e.;
After changing to Ex: - https://login.microsoftonline.com/contoso.onmicrosoft.com (or tenant id),
please save changes ,refresh portal / everything and try again.
If still it shows the error , check if the Application is registered to the Azure AD Tenant as Multi Tenant Application.
Then if it still remains check if the account is actually on Azure
AD ,as this error can occur when the user credentials you are trying
to use does not belong to the same tenant where the application is
actually registered in.
If it is different tenant and you are trying to access from different
account, then you may need to change its supported account types to
any organizational directory or you need to check for correct
credentials. If not check everything or create a new app registration
.
Also please check this "Use a tenant-specific endpoint or configure the application to be multi-tenant" when signing into my Azure website for possible
ways to solve the issue.
Else you can raise a support request
References:
msal - MsalException: Applicationis not configured as a multi-tenant
application. Android - Stack Overflow
Use single-tenant Azure AD apps with Microsoft Graph Toolkit -
Waldek Mastykarz

Need help demystifying the new feature introduced on Microsoft.Graph 4.0.0

Question:
I am not sure if this falls under question or code review because the code works where I do not know if it is implemented correctly. But, do we need to acquire the access token from Microsoft.Graph using either silent or interactive modes? From what I can tell the answer is, No. (see Context below)
The new implementation seems to be drastically scaled down with the whole idea of silent and interactive token retrieval being removed. Is this correct?
using Azure.Identity;
using Microsoft.Graph;
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var scopes = new[] { "User.Read" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "SomeGuid";
// Value from app registration
var clientId = "SomeGuid";
var options = new InteractiveBrowserCredentialOptions
{
TenantId = tenantId,
ClientId = clientId,
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
// MUST be http://localhost or http://localhost:PORT
// See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/System-Browser-on-.Net-Core
RedirectUri = new Uri("http://localhost:1234"),
};
// https://learn.microsoft.com/dotnet/api/azure.identity.interactivebrowsercredential
var interactiveCredential = new InteractiveBrowserCredential(options);
var graphClient = new GraphServiceClient(interactiveCredential, scopes);
// Interactive browser login occurs here.
var me = graphClient.Me.Request().GetAsync().Result;
// Printing the results
Console.WriteLine("-------- Data from call to MS Graph --------");
Console.Write(Environment.NewLine);
Console.WriteLine($"Id: {me.Id}");
Console.WriteLine($"Display Name: {me.DisplayName}");
Console.WriteLine($"Email: {me.Mail}");
//Console.ReadLine();
}
}
}
Context:
As part of our routine maintenance, I was tasked with upgrading our NuGet packages on a Winforms desktop application that is running in Azure and whose users are in Azure Active Directory Services (AADS). One of the packages, Microsoft.Graph, had a major version change. https://www.nuget.org/packages/Microsoft.Graph/4.0.0
The documentation on it indicated a new feature for handling the TokenCredentialClass. https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/4.0.0/docs/upgrade-to-v4.md#new-capabilities
From what I can tell, there is a separate and distinct break on how the token is retrieved. Previously, we followed the method provided here: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-windows-desktop#add-the-code-to-initialize-msal
Old way:
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
_PublicClientApp = PublicClientApplicationBuilder.Create(ClientId)
.WithRedirectUri("http://localhost:1234")
.WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
.Build();
// We sign the user in here
bolIsAutorizeSSO = CallMicrosoftSSO().GetAwaiter().GetResult();
InteractiveAuthenticationProvider = new InteractiveAuthenticationProvider(PublicClientApp, Scopes);
GraphServiceClient = new Microsoft.Graph.GraphServiceClient(InteractiveAuthenticationProvider);
if (bolIsAutorizeSSO)
{
// We also signt the user in here.
var User = GraphServiceClient.Me.Request().GetAsync().Result;
// Printing the results
Console.WriteLine("-------- Data from call to MS Graph --------");
Console.Write(Environment.NewLine);
Console.WriteLine($"Id: {User.Id}");
Console.WriteLine($"Display Name: {User.DisplayName}");
Console.WriteLine($"Email: {User.Mail}");
}
else
{
// signout
Console.ReadLine();
}
}
public static async Task<bool> CallMicrosoftSSO()
{
AuthenticationResult authResult = null;
var app = PublicClientApp;
var accounts = await app.GetAccountsAsync();
try
{
authResult = await app.AcquireTokenInteractive(Scopes)
.WithAccount(accounts.FirstOrDefault())
.WithPrompt(Microsoft.Identity.Client.Prompt.ForceLogin)
.ExecuteAsync();
}
catch (MsalUiRequiredException _Exception)
{
// A MsalUiRequiredException happened on AcquireTokenSilent.
// This indicates you need to call AcquireTokenInteractive to acquire a token.
Console.WriteLine(_Exception.Message);
}
catch (MsalException msalex)
{
if (msalex.ErrorCode != "authentication_canceled")
{
Console.WriteLine(msalex.Message);
}
}
catch (Exception _Exception)
{
Console.WriteLine(_Exception.Message);
}
if (authResult != null)
{
return true;
}
return false;
}
private static string ClientId = "SomeGuid";
private static string TenantId = "SomeGuid";
private static string[] Scopes = new string[] { "User.Read" };
private static Microsoft.Graph.GraphServiceClient GraphServiceClient;
private static bool bolIsAutorizeSSO = false;
private static InteractiveAuthenticationProvider InteractiveAuthenticationProvider;
private static IPublicClientApplication _PublicClientApp;
public static IPublicClientApplication PublicClientApp { get { return _PublicClientApp; } }
}
}
I am struggling to make sense of it. Partly because the feature is brand new and there are very few code samples up on the internet that say do it this way. What I have found seems to point me back to what we already are using (more on that in a bit). So, the examples may not yet be fully updated.

B2C reset password

I have following in my startup:
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
// B2C policy identifiers
// public static string SignUpPolicyId = ConfigurationManager.AppSettings["ida:SignUpPolicyId"];
public static string SignInUpPolicyId = ConfigurationManager.AppSettings["ida:SignInUpPolicyId"];
public static string DefaultPolicy = SignInUpPolicyId;
public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];
public static string ProfilePolicyId = ConfigurationManager.AppSettings["ida:UserProfilePolicyId"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Configure OpenID Connect middleware for each policy
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInUpPolicyId));
//app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(ResetPasswordPolicyId));
// app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInPolicyId));
}
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
notification.Response.Redirect("/Account/ResetPassword");
}
else if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var policy = notification.OwinContext.Get<string>("Policy");
if (!string.IsNullOrEmpty(policy) && !policy.Equals(DefaultPolicy))
{
//notification.ProtocolMessage.Scope = OpenIdConnectScopes.OpenId;
//notification.ProtocolMessage.ResponseType = OpenIdConnectResponseTypes.IdToken;
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.Replace(DefaultPolicy, policy);
}
return Task.FromResult(0);
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = String.Format(aadInstance, tenant, policy),
AuthenticationType = policy,
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = clientId,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
},
Scope = "openid",
ResponseType = "id_token",
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
SaveSigninToken = true //important to save the token in boostrapcontext
}
};
}
}
In my Controller I have following:
public void ResetPassword()
{
// Let the middleware know you are trying to use the reset password
policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
HttpContext.GetOwinContext().Set("Policy", Startup.ResetPasswordPolicyId);
// Set the page to redirect to after changing passwords
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
}
Right now the reset password just redirect to "/". I am not sure how this works, and havent been able to find any samples. I tried following this sample but it uses some cores libraries and havent succeeded using the documentation.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-devquickstarts-web-dotnet-susi
After following Chris suggestion it worked. See image below when clicking reset password.
Follow this Startup.Auth.cs file as closely as possible to get yourself started.
The ConfigureAuth method of the Startup class registers the OWIN OpenID Connect middleware that enables an ASP.NET MVC controller to set the Azure AD B2C policy, to be redirected to, using the OWIN context.
Example:
HttpContext.GetOwinContext().Set("Policy", Startup.ResetPasswordPolicyId);

ASP.NET Core MVC 6 Azure B2C Active Directory Authentication issue

I followed the article : https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore-b2c
In this sample app there is a Sign-in button. I am able to Sign-in successfully by clicking Sign-In button by providing my Azure B2C Tenant and registering the application in the tenant.
In another app, I want to authenticate without the Sign-In button being clicked i.e. right when I open the URL, I get redirected first to the Azure B2C AD login page, and after successful validation of credentials, I should be able to see the home screen.
So, what I did was from the URL mentioned from the article, I copied the SiginIn() method as:
public async Task<IActionResult> Index()
{
await SignIn();
await GetDataAsync();
}
I get an error message on running the application as : InvalidOperationException: No authentication handler is configured to handle the scheme: b2c_1_org_b2c_global_signin
Please advise how can I authenticate directly without the signin button. Previously with MVC5, I have successfully done this where I used [Authorize] attribute on the Controller class.
Controller Code with Index method
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Net;
using System.Text;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Hosting;
using WebViewerCore.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Authorization;
namespace WebViewerCore.Controllers
{
[Authorize]
public class DocumentController : Controller
{
#region GlobalVariables
private static readonly string serviceUrl = "";
private string doctype = string.Empty;
private string dmsno = string.Empty;
public string documentName = string.Empty;
private string errMsg = string.Empty;
StringBuilder msg;
Document doc;
private IHostingEnvironment _env;
private IConfiguration _config;
#endregion
#region C'tor
public DocumentController(IHostingEnvironment env, IConfiguration config)
{
_env = env;
_config = config;
}
#endregion
#region ControllerAction
public async Task<IActionResult> Index()
{
//return View();
try
{
//await SignIn();
string storageAccount = _config.GetSection("BlobStorage").GetSection("StorageAccount").Value;
string storageContainer = _config.GetSection("BlobStorage").GetSection("StorageContainer").Value;
ViewBag.StorageAccount = storageAccount;
ViewBag.StorageContainer = storageContainer;
await GetDataAsync();
//HttpContext.Response.ContentType = "application/vnd.ms-xpsdocument";
if (TempData["QueryStringMissing"] != null && (bool)TempData["QueryStringMissing"] || doc == null)
{
return View("View");
}
else
{
return View("Index", doc);
}
}
catch (Exception ex)
{
//logger.LogErrorWithMessage(ex, ex.StackTrace);
//return View("Error", new HandleErrorInfo(ex, "Document", "Index"));
throw ex;
}
}
#endregion
Startup.cs code
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.AspNetCore.Http;
using System.IO;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace WebViewerCore
{
public class Startup
{
#region Global Variables
public static string SignUpPolicyId;
public static string SignInPolicyId;
public static string ProfilePolicyId;
public static string ClientId;
public static string RedirectUri;
public static string AadInstance;
public static string Tenant;
#endregion
public Startup(IHostingEnvironment env)
{
//var builder = new ConfigurationBuilder()
// .SetBasePath(env.ContentRootPath)
// .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
// .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
// .AddEnvironmentVariables();
//Configuration = builder.Build();
Configuration = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.Build();
}
public IConfigurationRoot Configuration { get; set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
// Adds a default in-memory implementation of IDistributedCache.
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.CookieHttpOnly = true;
});
services.AddSingleton<IConfiguration>(Configuration);
// Add Authentication services.
services.AddAuthentication(sharedOptions => sharedOptions.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(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Document}/{action=Index}/{id?}");
});
// App config settings
ClientId = Configuration["AzureAD:ClientId"];
AadInstance = Configuration["AzureAD:AadInstance"];
Tenant = Configuration["AzureAD:Tenant"];
RedirectUri = Configuration["AzureAD:RedirectUri"];
// B2C policy identifiers
SignUpPolicyId = Configuration["AzureAD:SignUpPolicyId"];
SignInPolicyId = Configuration["AzureAD:SignInPolicyId"];
// Configure the OWIN pipeline to use OpenID Connect auth.
//app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignUpPolicyId));
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInPolicyId));
}
private OpenIdConnectOptions CreateOptionsFromPolicy(string policy)
{
policy = policy.ToLower();
return new OpenIdConnectOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = string.Format(AadInstance, Tenant, policy),
AuthenticationScheme = policy,
CallbackPath = new PathString(string.Format("/{0}", policy)),
// These are standard OpenID Connect parameters, with values pulled from config.json
ClientId = ClientId,
PostLogoutRedirectUri = RedirectUri,
Events = new OpenIdConnectEvents
{
OnRemoteFailure = RemoteFailure,
},
ResponseType = OpenIdConnectResponseType.IdToken,
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
}
};
}
// Used for avoiding yellow-screen-of-death
private Task RemoteFailure(FailureContext context)
{
context.HandleResponse();
if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied"))
{
context.Response.Redirect("/");
}
else
{
context.Response.Redirect("/Home/Error?message=" + context.Failure.Message);
}
return Task.FromResult(0);
}
}
}
To automatically redirect users navigating to a specific controller or endpoint in MVC, all you need to do is add the [Authorize] attribute, provided you've configured your middleware correctly.
In the case of Azure AD B2C, you need to make sure you add the OpenID Middleware like so:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = string.Format(AadInstance, Tenant, policy),
AuthenticationScheme = policy,
CallbackPath = new PathString(string.Format("/{0}", policy)),
// These are standard OpenID Connect parameters, with values pulled from config.json
ClientId = ClientId,
PostLogoutRedirectUri = RedirectUri,
Events = new OpenIdConnectEvents
{
OnRemoteFailure = RemoteFailure,
},
ResponseType = OpenIdConnectResponseType.IdToken,
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
},
};);
The sample you referenced already does what you want, to a point.
In that sample, if you haven't signed in and navigate to /about, you'll get automatically redirected to Azure AD B2C to sign in.
As it stands, the sample has the [Authorize] attribute only on a controller action in the Home controller, you'll want to move that up to the controller level and add it to every controller. Then you can just remove the the sign-in/sign-up/sign-out buttons and controller action.

NullReferenceException in Microsoft.IdentityModel.Clients.ActiveDirectory.AcquireToken

I'm trying to get some MSFT Power BI SDK samples working. Unfortunately, the Microsoft.IdentityModel.Clients.ActiveDirectory library is giving me a lot of trouble with the initial external authentication step.
I'm using Microsoft.IdentityModel.Clients.ActiveDirectory, Version=2.28.3.860, from NuGet; this is the last version of the library before AcquireToken was removed, and I haven't figured out how to use its replacement (AcquireTokenAsync) in a way that's equivalent to what I see in the samples.
When I take the following code and modify the TODO lines to specify my actual Azure Client ID and authentication redirect page, I get as far as the AcquireToken line.
using System;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
public class Application
{
public static void Main(string[] args)
{
try
{
string clientID = "abcdef01-1234-1234-abcd-abcdabcd1234"; // TODO: actual Azure client ID
string redirectUri = "https://acmecorporation.okta.com/login/do-login"; // TODO: actual redirect
string resourceUri = "https://analysis.windows.net/powerbi/api";
string authorityUri = "https://login.windows.net/common/oauth2/authorize";
AuthenticationContext authContext = new AuthenticationContext(authorityUri);
AuthenticationResult ar = authContext.AcquireToken(
resourceUri,
clientID,
new Uri(redirectUri),
PromptBehavior.RefreshSession);
string token = ar.AccessToken;
Console.WriteLine("Success: " + token);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
At this point:
A "Sign in to your account" window pops up with the name of the app I've associated in Azure with the clientID GUID
I'm redirected to my organization's ("acmecorporation") sign-on page
I sign in using my AD credentials
The AcquireToken method throws the following NullReferenceExpection:
System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext.RunAsyncTask[T](Task`1 task)
at Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext.AcquireToken(String resource, String clientId, Uri redirectUri, PromptBehavior promptBehavior)
at PowerBISample.Application.Main(String[] args) in \\noxfile\users\ehirst\documents\visual studio 2015\Projects\PowerBISample\PowerBISample\Program.cs:line 18
Can anyone provide guidance on how to get past this? My goal is to get a POC working to determine whether we can integrate Power BI into a larger application, but so far it feels like I'm beta testing a pretty unstable system.
The NullReferenceException is a bug in the 2.x version of the ADAL library; it's fixed in current versions. It was triggered by an incorrect value of redirectUri; unfortunately the documentation was sparse on this one. A working code sample, adapted (thanks Kanishk!) to use the current 3.13.7 version of ADAL, is posted below.
namespace PowerBISample
{
using System;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Threading.Tasks;
public class Application
{
public static void Main(string[] args)
{
Run();
Console.ReadLine();
}
static async void Run()
{
try
{
string clientID = "abcdef01-1234-1234-abcd-abcdabcd1234"; // TODO: actual Azure client ID
/** THE REAL PROBLEM WAS HERE **/
string redirectUri = "https://login.live.com/oauth20_desktop.srf";
string resourceUri = "https://analysis.windows.net/powerbi/api";
string authorityUri = "https://login.windows.net/common/oauth2/authorize";
AuthenticationContext authContext = new AuthenticationContext(authorityUri);
AuthenticationResult ar = await authContext.AcquireTokenAsync(resourceUri, clientID, new Uri(redirectUri), new PlatformParameters(PromptBehavior.RefreshSession));
string token = ar.AccessToken;
Console.WriteLine("Success: " + token);
}
catch (Exception ex)
{
string error = ex.ToString();
Console.WriteLine(error);
}
}
}
}

Resources