The web application should allow internal employees with AD accounts to authenticate in the app using Azure AD Authentication. External users should be able to register and sign in using ASP.NET Core Identity. I can implement each one separately but not together in the same app. When I add both authentications to the same app, the ASP.NET Core Identity works perfectly. I can register and log in using Identity with no problem. However when I try to log in with Azure AD, the app redirects me to my tenant's login page, I submit a username and password, it redirects me back to the application but no user is authenticated. I hit the login button again and the same thing happens. It seems that the web app or browser is not saving the access token or something like that.
What am I doing wrong? Is it even possible to have two sets of authentication on the same app?
Thanks. Here's the code:
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.1">
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration) => Configuration = configuration;
public void ConfigureServices(IServiceCollection services)
{
// Add Azure AD authentication
services.AddAuthentication(defaultScheme: AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
// Add the application db context
services.AddDbContext<AppDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
// Add Identity using Entity Framework Core
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// Configure Identity
services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
options.User.RequireUniqueEmail = true;
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
This is a custom controller where we handle HTTP requests related to authentication.
private readonly UserManager<ApplicationUser> userManager;
private readonly SignInManager<ApplicationUser> signInManager;
public UserController(UserManager<ApplicationUser> um, SignInManager<ApplicationUser> sm) =>
(userManager, signInManager) = (um, sm);
// Internal employee users will authenticate using Azure AD
[HttpGet("internal-signin")]
public ChallengeResult InternalSignIn(string returnUrl = "/") =>
Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, AzureADDefaults.AuthenticationScheme);
// Display view with a form to create a new external user account
[HttpGet("register")]
public ViewResult Register() => View();
// Create a new account for an external user
[HttpPost("register")]
public async Task<IActionResult> Register(RegistrationInputModel inputModel)
{
// Check if the model state is valid
if (!ModelState.IsValid)
{
// Redirect to the Register view
return View(viewName: nameof(Register), model: inputModel);
}
// Create an application user object
ApplicationUser user = new ApplicationUser
{
// Map the fields of the input model with the user
UserName = inputModel.Email,
Email = inputModel.Email,
FirstName = inputModel.FirstName,
LastName = inputModel.LastName,
Company = inputModel.CompanyName,
};
// Try to register the user on the database
IdentityResult result = await userManager.CreateAsync(user, inputModel.Password);
// If failed, then set the error messages into the model state
if (!result.Succeeded)
{
foreach (IdentityError error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
// Return the user to the registration view
return View(viewName: nameof(Register), model: inputModel);
}
// Sign In the user
await signInManager.SignInAsync(user, isPersistent: false);
// Otherwise, redirect the user to the index page
return RedirectToAction(nameof(HomeController.Index), controllerName: "Home");
}
// External users sign out action
[HttpGet("signout")]
[Authorize]
public async Task<IActionResult> SignOut()
{
await signInManager.SignOutAsync();
return RedirectToAction(nameof(HomeController.Index), "Home");
}
// Display form to login for external users
[HttpGet("signin")]
public ViewResult SignIn() => View();
// Login an external user
[HttpPost("signin")]
public async Task<IActionResult> SingIn(SingInInputModel inputModel)
{
// Check if the model state is valid
if (!ModelState.IsValid)
{
// Send the user back to the sign in view
return View(viewName: nameof(SignIn), model: inputModel);
}
// Try to sign in the user
SignInResult result = await signInManager
.PasswordSignInAsync(inputModel.Email, inputModel.Password, inputModel.RememberMe, lockoutOnFailure: false);
// Check if the login was unsuccessful
if (!result.Succeeded)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(viewName: nameof(SignIn), model: inputModel);
}
// Send the user back to the index page
return RedirectToAction(nameof(HomeController.Index), "Home");
}
public class ApplicationUser : Microsoft.AspNetCore.Identity.IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Company { get; set; }
}
Select ASP.NET Core Web Application>Choose Web Application (Model-View-Controller) template> Click on the "Change Authentication" button>Select "Work or School Accounts". Choose Cloud - Single Organization. Fill up the field of Domain which is the Azure Active Directory tenant name (say, softdreams.onmicrosoft.com).
Azure AD supports many standardized protocols for authentication and authorization, such as SAML 2.0, OpenID Connect, OAuth 2.0, and WS-Federation. Azure AD also supports password vaulting and automated sign-in capabilities for apps that only support forms-based authentication.
If using ASP.NET Core Identity with Azure AD login , you can set CookieSchemeName
to Identity.External
so that asp.net core identity can get the external user profile from external identity provider , and create a local user associated with external user :
In appsettings.json :
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "peterpad.onmicrosoft.com",
"TenantId": "cb1c3f2e-a2dd-4fde-bf8f-f75ab18b21ac",
"ClientId": "0c0ec562-a9bb-4722-b615-6dcbdc646326",
"CallbackPath": "/signin-oidc",
"CookieSchemeName": "Identity.External"
},
Then if you want to challenge Azure AD login in MVC controller , you should provide the scheme name , config redirect url after authentication to Identity/Account/ExternalLogin
and Callback
handler , in that handler asp.net core identity will let your enter username and create a local user :
[HttpGet("internal-signin")]
public ChallengeResult InternalSignIn(string returnUrl = "/")
{
var redirectUrl = Url.Page("/Account/ExternalLogin", pageHandler: "Callback", values: new { returnUrl , area = "Identity" });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(AzureADDefaults.AuthenticationScheme, redirectUrl);
return new ChallengeResult(AzureADDefaults.AuthenticationScheme, properties);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With