Windows IdentityServer 的身份验证不会在没有错误消息的情况下进行登录

Windows Authentication for IdentityServer doesn't take login without error message

我已将我的身份服务器配置为使用 Windows 身份验证。我的问题是在提交我的 Windows 登录后,浏览器中的 Windows 安全登录框不断返回。看起来用户名或密码不正确,但没有错误消息:

我尝试了不同格式的凭据,例如用户名中有“\MyDomain”和没有,但没有任何区别。我知道我的 Windows 凭据是正确的,因为我每天都用它来登录网络。我也尝试了不同的浏览器 - Edge 和 Chrome,也没有区别。有谁知道会发生什么?我该如何调试这样的问题?

更新

刚注意到我的问题可能是因为我在 ExternalController 中的 Challenge(string scheme, string returnUrl) 函数被重复调用。这是函数:

[HttpGet]
public IActionResult Challenge(string scheme, string returnUrl)
{
    if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";

    // validate returnUrl - either it is a valid OIDC URL or back to a local page
    if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
    {
        // user might have clicked on a malicious link - should be logged
        throw new Exception("invalid return URL");
    }
    
    // start challenge and roundtrip the return URL and scheme 
    var props = new AuthenticationProperties
    {
        RedirectUri = Url.Action(nameof(Callback)), 
        Items =
        {
            { "returnUrl", returnUrl }, 
            { "scheme", scheme },
        }
    };
    return Challenge(props, scheme);
}

当我 运行 它在调试时,这个函数在选择 Windows 身份验证选项之后和浏览器上显示 Windows 安全登录框之前被调用两次,然后,登录提交后。但是调用 HttpContext.AuthenticateAsync 的 Callback() 函数永远不会被调用。没有我可以追踪的调用者的源代码。谁在调用这个函数?为什么提交登录后又调用了?我在这里做错了什么?

Update-1

除了我在上面发布的 Challenge 函数之外,这是我修改的登录函数(通过在 ChallengeWindowsAsync 顶部添加调用)和 ChallengeWindowsAsync(returnUrl)我添加到 AccountController.cs 的功能。后来我注释掉了 ChallengeWindowsAsync 的调用,因为我认为不需要它,因为 ExternalController.cs 中的 Challenge(string scheme, string returnUrl) 函数负责 Windows 身份验证。

[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
    //// trigger Windows authentication by calling ChallengeAsync
    //await ChallengeWindowsAsync(returnUrl);

    // build a model so we know what to show on the login page
    var vm = await BuildLoginViewModelAsync(returnUrl);

    if (vm.IsExternalLoginOnly)
    {
        // we only have one option for logging in and it's an external provider
        return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl });
    }

    return View(vm);
}

private async Task<IActionResult> ChallengeWindowsAsync(string returnUrl)
{
    // see if windows auth has already been requested and succeeded
    var result = await HttpContext.AuthenticateAsync("Windows");
    if (result?.Principal is WindowsPrincipal wp)
    {
        // we will issue the external cookie and then redirect the
        // user back to the external callback, in essence, treating windows
        // auth the same as any other external authentication mechanism
        var props = new AuthenticationProperties()
        {
            RedirectUri = Url.Action("Callback"),
            Items =
            {
                { "returnUrl", returnUrl },
                { "scheme", "Windows" },
            }
        };

        var id = new ClaimsIdentity("Windows");

        // the sid is a good sub value
        id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.FindFirst(ClaimTypes.PrimarySid).Value));

        // the account name is the closest we have to a display name
        id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));

        // add the groups as claims -- be careful if the number of groups is too large
        var wi = wp.Identity as WindowsIdentity;

        // translate group SIDs to display names
        var groups = wi.Groups.Translate(typeof(NTAccount));
        var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
        id.AddClaims(roles);


        await HttpContext.SignInAsync(
            IdentityServerConstants.ExternalCookieAuthenticationScheme,
            new ClaimsPrincipal(id),
            props);
        return Redirect(props.RedirectUri);
    }
    else
    {
        // trigger windows auth
        // since windows auth don't support the redirect uri,
        // this URL is re-triggered when we call challenge
        return Challenge("Windows");
    }
}

Update-2

修改了我的 Login() 和 Challenge() 如下,但仍然有同样的问题 - Windows 安全登录框不断出现。我还注意到 Login() 函数只被调用一次——在显示身份验证选项的 IdentitySever 页面之前。之后,只有 Challenge() 被重复调用。我做错了什么?

[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
    // build a model so we know what to show on the login page
    var vm = await BuildLoginViewModelAsync(returnUrl);

    if (vm.ExternalLoginScheme == "Windows")
    {
        var authenticationResult = await HttpContext.AuthenticateAsync("Windows").ConfigureAwait(false);
        if (authenticationResult.Succeeded && authenticationResult?.Principal is WindowsPrincipal windowsPrinciple)
        {
            // Add your custom code here
            var authProps = new AuthenticationProperties()
            {
                RedirectUri = Url.Action("Callback"),
                Items =
                {
                    { "returnUrl", returnUrl },
                    { "scheme", "Windows"},
                }
            };

            await HttpContext.SignInAsync(authenticationResult.Principal);
            return Redirect(authProps.RedirectUri);
        }
        else
        {
            return Challenge("Windows");
        }
    }
    return View(vm);
}

[HttpGet]
public IActionResult Challenge(string scheme, string returnUrl)
{
    if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";

    // validate returnUrl - either it is a valid OIDC URL or back to a local page
    if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
    {
        // user might have clicked on a malicious link - should be logged
        throw new Exception("invalid return URL");
    }
    
    // start challenge and roundtrip the return URL and scheme 
    var props = new AuthenticationProperties
    {
        RedirectUri = Url.Action(nameof(Callback)), 
        Items =
        {
            { "returnUrl", returnUrl }, 
            { "scheme", scheme },
        }
    };

    return Challenge(props, scheme);
    
}

Update-3 这是我的 Login.cshtml 文件

@model LoginViewModel

<div class="login-page">
    <div class="lead">
        <h1>Login</h1>
        <p>Choose how to login</p>
    </div>

    <partial name="_ValidationSummary" />

    <div class="row">

        @if (Model.EnableLocalLogin)
        {
            <div class="col-sm-6">
                <div class="card">
                    <div class="card-header">
                        <h2>Local Account</h2>
                    </div>

                    <div class="card-body">
                        <form asp-route="Login">
                            <input type="hidden" asp-for="ReturnUrl" />

                            <div class="form-group">
                                <label asp-for="Username"></label>
                                <input class="form-control" placeholder="Username" asp-for="Username" autofocus>
                            </div>
                            <div class="form-group">
                                <label asp-for="Password"></label>
                                <input type="password" class="form-control" placeholder="Password" asp-for="Password" autocomplete="off">
                            </div>
                            @if (Model.AllowRememberLogin)
                            {
                                <div class="form-group">
                                    <div class="form-check">
                                        <input class="form-check-input" asp-for="RememberLogin">
                                        <label class="form-check-label" asp-for="RememberLogin">
                                            Remember My Login
                                        </label>
                                    </div>
                                </div>
                            }

                            <div>
                                <p>The default users are alice/bob, password: Pass123$</p>
                            </div>

                            <button class="btn btn-primary" name="button" value="login">Login</button>
                            <button class="btn btn-secondary" name="button" value="cancel">Cancel</button>
                        </form>
                    </div>
                </div>
            </div>
        }

        @if (Model.VisibleExternalProviders.Any())
        {
            <div class="col-sm-6">
                <div class="card">
                    <div class="card-header">
                        <h2>External Account</h2>
                    </div>
                    <div class="card-body">
                        <ul class="list-inline">
                            @foreach (var provider in Model.VisibleExternalProviders)
                            {
                                <li class="list-inline-item">
                                    <a class="btn btn-secondary"
                                       asp-controller="External"
                                       asp-action="Challenge"
                                       asp-route-scheme="@provider.AuthenticationScheme"
                                       asp-route-returnUrl="@Model.ReturnUrl">
                                        @provider.DisplayName
                                    </a>
                                </li>
                            }
                        </ul>
                    </div>
                </div>
            </div>
        }

        @if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any())
        {
            <div class="alert alert-warning">
                <strong>Invalid login request</strong>
                There are no login schemes configured for this request.
            </div>
        }
    </div>
</div>

Update-4

这是我的 Startup.cs 的 ConfigureServices 和 Configure 功能:

public void ConfigureServices(IServiceCollection services)
{
    IdentityModelEventSource.ShowPII = true;
    services.AddControllersWithViews();

    var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

    var connstr = Configuration.GetConnectionString("IDSConnection");
    services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connstr));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    var builder = services.AddIdentityServer(options =>
    {
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;

        options.EmitStaticAudienceClaim = true;
    })
        .AddAspNetIdentity<ApplicationUser>()
        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = b => b.UseSqlServer(connstr,
                sql => sql.MigrationsAssembly(migrationsAssembly));
        })
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = b => b.UseSqlServer(connstr,
                sql => sql.MigrationsAssembly(migrationsAssembly));
        });

    // not recommended for production - you need to store your key material somewhere secure
    builder.AddDeveloperSigningCredential();

    // configures IIS in-proc settings
    services.Configure<IISServerOptions>(iis =>
    {
        iis.AuthenticationDisplayName = "Windows";
        iis.AutomaticAuthentication = false;
    });

    services.AddAuthentication(IISDefaults.AuthenticationScheme); // for Windows authentication.
    services.AddAuthentication() // for "Google" login.
        .AddGoogle(options =>
        {
            options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

            options.ClientId = "....apps.googleusercontent.com";
            options.ClientSecret = "....";
        });
}

public void Configure(IApplicationBuilder app)
{
    if (Environment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }

    // uncomment if use MVC
    app.UseStaticFiles();
    app.UseRouting();

    // Add IdentityServer to the pipeline
    // UseIdentityServer includes a call to UseAuthentication, so it’s not necessary to have both
    app.UseIdentityServer();
    // uncomment if use MVC
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

在调用Challenge() 方法之前,请确保用户是否已经登录。请参阅下面的示例代码。

[HttpGet] public async Task<IActionResult> Login(string returnUrl)
{
 if(loginViewModel.ExternalLoginScheme == "Windows")
 {
   var authenticationResult = await HttpContext.AuthenticateAsync("Windows").ConfigureAwait(false);
   if (authenticationResult.Succeeded && authenticationResult?.Principal is WindowsPrincipal windowsPrinciple)
   {
     // Add your custom code here
     var authProps = new AuthenticationProperties()
                {
                    RedirectUri = Url.Action("Callback"),
                    Items =
                    {
                        { "returnUrl", returnUrl },
                        { "scheme", "Windows"},
                    }
                };

    await HttpContext.SignInAsync();

    return Redirect(RedirectUri);
   }
   else
   {
     return Challenge("Windows");
   }
  }
}

更新:

我遇到了你的问题并找到了解决方案,请查看下面的代码并更新你的 startup.cs 文件。

services.Configure<IISServerOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows";
                iis.AutomaticAuthentication = false;
            });

参考资料:阅读 ID4 文档 here

我终于想通了这个问题。我知道有很多事情会导致相同或相似的症状,但 修复了我的问题。