Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use both Azure AD authentication and Identity on ASP.NET Core 3?

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:

Packages

<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">

Startup Class

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());
}

User Controller

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");
}

Application User

public class ApplicationUser : Microsoft.AspNetCore.Identity.IdentityUser
{
     public string FirstName { get; set; }
     public string LastName { get; set; }
     public string Company { get; set; }
}
like image 821
Jossean Yamil Avatar asked Feb 12 '20 23:02

Jossean Yamil


People also ask

How do I add Azure AD authentication to existing ASP.NET Core application?

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).

Which two authentication methods are available for SAAS applications on Azure AD?

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.


Video Answer


1 Answers

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/ExternalLoginand Callbackhandler , 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);
}
like image 75
Nan Yu Avatar answered Oct 26 '22 02:10

Nan Yu