如何在 ASP.NET Core 3 上同时使用 Azure AD 身份验证和身份?

Posted

技术标签:

【中文标题】如何在 ASP.NET Core 3 上同时使用 Azure AD 身份验证和身份?【英文标题】:How to use both Azure AD authentication and Identity on ASP.NET Core 3? 【发布时间】:2020-05-28 14:49:45 【问题描述】:

Web 应用程序应允许拥有 AD 帐户的内部员工使用 Azure AD 身份验证在应用程序中进行身份验证。外部用户应该能够使用 ASP.NET Core Identity 进行注册和登录。我可以单独实现每一个,但不能在同一个应用程序中一起实现。当我将两种身份验证添加到同一个应用程序时,ASP.NET Core Identity 可以完美运行。我可以毫无问题地使用身份注册和登录。但是,当我尝试使用 Azure AD 登录时,应用程序将我重定向到租户的登录页面,我提交了用户名和密码,它将我重定向回应用程序,但没有用户通过身份验证。我再次点击登录按钮,同样的事情发生了。似乎网络应用程序或浏览器没有保存访问令牌或类似的东西。

我做错了什么?甚至可以在同一个应用程序上进行两组身份验证吗?

谢谢。代码如下:

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

用户控制器

这是一个自定义控制器,我们在其中处理与身份验证相关的 HTTP 请求。

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; 

【问题讨论】:

使用 Azure AD B2C 不是更好的解决方案吗?它具有多个 OAuth 选项,以及与来自不同组织的不同 AD 组协作的选项。所有这些都是在 Azure 云上完成的,因此可以节省大量时间。 【参考方案1】:

如果使用带有 Azure AD 登录的 ASP.NET Core Identity ,您可以将 CookieSchemeName 设置为 Identity.External 以便 asp.net core identity 可以从外部身份提供者获取外部用户配置文件,并创建与关联的本地用户外部用户:

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

然后,如果您想在 MVC 控制器中挑战 Azure AD 登录,您应该提供方案名称,在身份验证后配置重定向 url 到 Identity/Account/ExternalLoginCallbackhandler,在该处理程序中 asp.net 核心身份将让您进入用户名并创建本地用户:

[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);

【讨论】:

这行得通。但是,我没有使用 VS 为您生成的身份页面,因此我向名为 InternalSingInCallbackUserController 添加了一个回调,并编写了类似于 Account/ExternalLogin 页面正在执行的代码。我不得不将 redirectUrl 变量更改为以下内容:string redirectUrl = Url.Action(nameof(InternalSignInCallback)); 希望我能投票一百万次!我只是在这上面浪费了一整天...

以上是关于如何在 ASP.NET Core 3 上同时使用 Azure AD 身份验证和身份?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Visual Studio 2019 在 Docker 容器中运行 ASP.NET Core 3.1 项目?

.NET Core 3.0及ASP.NET Core 3.0 前瞻

如何在 ASP.NET Core 中使用 NLog 的高级特性

ASP.Net Core 3 远程证书在 MacOs 上无效

asp.net core 使用EF7 Code First 创建数据库,同时使用命令创建数据库(本来想数据迁移 没有成功,只能将标题搞成这个。)

如何在 ASP.NET Core 3.1 中使用 Java?