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。
我终于想通了这个问题。我知道有很多事情会导致相同或相似的症状,但 修复了我的问题。
我已将我的身份服务器配置为使用 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。
我终于想通了这个问题。我知道有很多事情会导致相同或相似的症状,但