1

I have an ASP.NET Core 5 app that uses the Identity framework on a Windows server. When the user clicks Submit on the Login page, it needs to

  1. Authenticate their username and password against AD #1.
  2. If that fails, it then needs to authenticate them against AD #2.
  3. If that also fails, it should give an error.

This post covers the AD authentication, but I have no idea where it should go or how it's triggered: https://stackoverflow.com/a/49742910/177416

This answer provides details on creating a custom authentication but how does that integrate into the Identity framework, which already has a UserManager? https://stackoverflow.com/a/49047358/177416

If all I want to do is lock down the app with authentication, do I need the Identity library? Is there a simpler way to do this?

Thank you.

Update: See answer below for RazorPages. Similar logic can be used for MVC version.

Alex
  • 34,699
  • 13
  • 75
  • 158
  • A server can't be in two Active Directory domains. A user in one domain can use resources (including servers) on another domain if there's a trust or federation relation between domains. There's nothing to trigger, this works automatically if eg you try to access a file share or web server. For web apps on IIS, enabling Windows Authentication on the virtual directory is enough. – Panagiotis Kanavos Mar 10 '22 at 12:09
  • @PanagiotisKanavos, sorry for the poorly worded question: It's not on two domains. We have an AD that's for internal users and one for the external users across the state. – Alex Mar 10 '22 at 12:10
  • Is there a trust relation between the two? Or federation? Is it really a separate domain or a hybrid Azure AD domain? Do you use IIS? ASP.NET? Or ASP.NET Core? These things matter and are already documented. – Panagiotis Kanavos Mar 10 '22 at 12:12
  • @PanagiotisKanavos, I don't know if there's a trust relationship, though I can hit both. It's a separate domain. I use IIS with ASP.NET Core – Alex Mar 10 '22 at 12:13

1 Answers1

1

To get it working with RazorPages, we need to add the following in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Other statements...

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(x => x.LoginPath = "/login");
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ 
    // Other statements...
    
    app.UseAuthentication();
    app.UseAuthorization();
}

Then on the PageModel for the page we'd like to authenticate, we add the [Authorize] attribute to the class.

Then in the LoginModel PageModel:

public class LoginModel : PageModel
{
    [BindProperty]
    [DisplayName("Username:")]
    [Required]
    public string Username { get; set; }

    [BindProperty]
    [Required]
    [DataType(DataType.Password)]
    [DisplayName("Password:")]
    public string Password { get; set; }

    public string ReturnUrl { get; set; }

    public void OnGet(string returnUrl = null)
    {
        ReturnUrl = returnUrl;
    }

    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Check both LDAPs. User must authenticate against one
        var isAuthenticated = IsAuthenticated(ConfigurationManager.AppSettings["MyFirstLDAPPath"]) || IsAuthenticated(ConfigurationManager.AppSettings["MySecondLDAPPath"]);

        if (isAuthenticated)
        {
            // Must provide at least the Username claim or it will throw an InvalidOperationException
            var identity = new ClaimsIdentity(new List<Claim>{ new Claim(ClaimTypes.Name, Username, ClaimValueTypes.String, "SomeUniqueValue") }, CookieAuthenticationDefaults.AuthenticationScheme);

            var principal = new ClaimsPrincipal(identity);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal,
                new AuthenticationProperties());
            return LocalRedirect(returnUrl);
        }

        ViewData["Message"] = "Invalid credentials.";
        ReturnUrl = returnUrl;
        return Page();
    }

    // This is a reliable way to check the user's credentials.
    // Note that this also considers users with a changed password,
    // as some other techniques don't
    private bool IsAuthenticated(string ldapPath)
    {
        try
        {
            var conn = new LdapConnection(ldapPath);
            conn.Credential = new NetworkCredential(Username, Password);
            conn.Bind();
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }
}

Then in Login.cshtml:

<form asp-route-returnurl="@returnUrl" method="post">
    @Html.AntiForgeryToken()
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    @if (!string.IsNullOrEmpty(ViewData["Message"]?.ToString()))
    {
        <span class="text-danger">
            @ViewData["Message"]
        </span>
    }
    @Html.HiddenFor(x => x.ReturnUrl)
    <h3 class="text-center text-info">Login</h3>
    <div class="form-group">
        <label asp-for="Username" class="text-info"></label>
        <input asp-for="Username" class="form-control" autofocus/>
        <span asp-validation-for="Username" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Password" class="text-info"></label>
        <input asp-for="Password" class="form-control"/>
        <span asp-validation-for="Password" class="text-danger"></span>
    </div>
    <div class="form-group">
        <input type="submit" name="submit" class="btn btn-info btn-md" value="submit">
    </div>
</form>

Hope this helps others.

Alex
  • 34,699
  • 13
  • 75
  • 158