IdentityServer4 资源所有者密码和 Win 身份验证:未授权
IdentityServer4 Resource owner password and Win auth: unauthorized
在过去的几天里,我一直在阅读 IdentityServer4 文档并使用资源所有者密码将我的示例服务器 + 示例客户端放在一起。现在我想并行添加 Windows 身份验证(将通过 Active Directory 完成),因此客户端应用程序(不是网络应用程序而是桌面应用程序)可以提示用户输入凭据或使用 Windows 通过 Active Directory 进行身份验证。
关于 Windows 身份验证的文档解释了如何配置 IIS 或 HTTP.Sys,但我想要的是:
- 用户打开应用程序
- 应用程序使用单点登录 post 网络请求 api 请求令牌和刷新令牌
- 网络 api 使用 windows 身份验证来验证用户身份和 returns 令牌
我已尝试遵循此 answer,但它不起作用(它 returns 未经授权)。
网络api:Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
// base-address of your identityserver
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
// name of the API resource
options.Audience = "api/user";
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddIdentityServer(options => { options.PublicOrigin = "http://localhost:5000"; options.MutualTls.Enabled = false; })
.AddExtensionGrantValidator<WinAuthGrantValidator>()
.AddDeveloperSigningCredential()
.AddTestUsers(Config.GetUsers())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddInMemoryIdentityResources(Config.GetIdentityResources());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILogger<Startup> logger, IServer server)
{
app.Use(async (context, next) =>
{
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
.MaxRequestBodySize = 10 * 1024;
var serverAddressesFeature =
app.ServerFeatures.Get<IServerAddressesFeature>();
var addresses = string.Join(", ", serverAddressesFeature?.Addresses);
logger.LogInformation($"Addresses: {addresses}");
await next.Invoke();
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// Enable HTTPS Redirection Middleware when hosting the app securely.
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
app.UseIdentityServer();
app.UseAuthentication();
}
}
internal static class Config
{
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password"
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password"
}
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
// other clients omitted...
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
// AllowedScopes = { "api1" }
AllowedScopes = { "api/user" }
},
new Client
{
ClientId = "winauth",
AllowedGrantTypes = new List<string>{ "windows_auth" },
ClientSecrets =
{
new Secret("secret".Sha256())
},
// AllowedScopes = { "api1" }
AllowedScopes = { "api/user" }
}
};
}
internal static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource { Name = "api1",Scopes = new List<Scope> { new Scope { Name = "api1",
DisplayName = "Full access to API 2"} }, Enabled = true, ApiSecrets = new List<Secret>
{
new Secret("secret".Sha256())
}
},
new ApiResource { Name = "api/user",Scopes = new List<Scope> { new Scope { Name = "api/user",
DisplayName = "Full access to API 2"} }, Enabled = true, ApiSecrets = new List<Secret>
{
new Secret("secret".Sha256())
}
}};
}
public static List<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
}
public class WinAuthGrantValidator : IExtensionGrantValidator
{
private readonly HttpContext httpContext;
public string GrantType => "windows_auth";
public WinAuthGrantValidator(IHttpContextAccessor httpContextAccessor)
{
httpContext = httpContextAccessor.HttpContext;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
// see if windows auth has already been requested and succeeded
var result = await httpContext.AuthenticateAsync("Windows");
if (result?.Principal is WindowsPrincipal wp)
{
context.Result = new GrantValidationResult(wp.Identity.Name, GrantType, wp.Claims);
}
else
{
// trigger windows auth
await httpContext.ChallengeAsync("Windows");
context.Result = new GrantValidationResult { IsError = false, Error = null, Subject = null };
}
}
}
}
网络API:Program.cs
public class Program
{
public static void Main(string[] args)
{
var isService = !(Debugger.IsAttached || args.Contains("--console"));
if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);
Directory.SetCurrentDirectory(pathToContentRoot);
}
var builder = CreateWebHostBuilder(
args.Where(arg => arg != "--console").ToArray());
var host = builder.Build();
if (isService)
{
// To run the app without the CustomWebHostService change the
// next line to host.RunAsService();
host.RunAsCustomService();
}
else
{
host.Run();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddEventLog();
})
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.AllowSynchronousIO = true;
options.Authentication.Schemes = Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes.Kerberos | Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes.NTLM;
options.Authentication.AllowAnonymous = true;
options.MaxConnections = null;
options.MaxRequestBodySize = 30000000;
options.UrlPrefixes.Add("http://localhost:5000");
});
}
网络APIUserController.cs
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Bearer")]
[ApiController]
public class UserController : ControllerBase
{
// GET api/user
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { User.Identity.Name, User.Identity.AuthenticationType };
}
}
客户代码:
using (var client = new HttpClient())
{
disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = baseUrl,
Policy = { RequireHttps = false }
});
if (disco.IsError)
{
Console.WriteLine(disco.Error);
Console.ReadLine();
return;
}
var httpHandler = new HttpClientHandler
{
UseDefaultCredentials = true,
};
using (var client = new HttpClient())
{
// request token
TokenResponse tokenResponse = await client.RequestTokenAsync(new TokenRequest
{
GrantType = "windows_auth",
Address = disco.TokenEndpoint,
ClientId = "winauth",
ClientSecret = "secret"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
Console.ReadLine();
return;
}
}
它returns未经授权。
我找到了一个解决方案:我需要用 SubjectId = MYDOMAIN\myusername
配置一个 TestUser 然后它起作用了。
出现的错误过于混乱。
在过去的几天里,我一直在阅读 IdentityServer4 文档并使用资源所有者密码将我的示例服务器 + 示例客户端放在一起。现在我想并行添加 Windows 身份验证(将通过 Active Directory 完成),因此客户端应用程序(不是网络应用程序而是桌面应用程序)可以提示用户输入凭据或使用 Windows 通过 Active Directory 进行身份验证。
关于 Windows 身份验证的文档解释了如何配置 IIS 或 HTTP.Sys,但我想要的是:
- 用户打开应用程序
- 应用程序使用单点登录 post 网络请求 api 请求令牌和刷新令牌
- 网络 api 使用 windows 身份验证来验证用户身份和 returns 令牌
我已尝试遵循此 answer,但它不起作用(它 returns 未经授权)。
网络api:Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
// base-address of your identityserver
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
// name of the API resource
options.Audience = "api/user";
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddIdentityServer(options => { options.PublicOrigin = "http://localhost:5000"; options.MutualTls.Enabled = false; })
.AddExtensionGrantValidator<WinAuthGrantValidator>()
.AddDeveloperSigningCredential()
.AddTestUsers(Config.GetUsers())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddInMemoryIdentityResources(Config.GetIdentityResources());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILogger<Startup> logger, IServer server)
{
app.Use(async (context, next) =>
{
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
.MaxRequestBodySize = 10 * 1024;
var serverAddressesFeature =
app.ServerFeatures.Get<IServerAddressesFeature>();
var addresses = string.Join(", ", serverAddressesFeature?.Addresses);
logger.LogInformation($"Addresses: {addresses}");
await next.Invoke();
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// Enable HTTPS Redirection Middleware when hosting the app securely.
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
app.UseIdentityServer();
app.UseAuthentication();
}
}
internal static class Config
{
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password"
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password"
}
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
// other clients omitted...
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
// AllowedScopes = { "api1" }
AllowedScopes = { "api/user" }
},
new Client
{
ClientId = "winauth",
AllowedGrantTypes = new List<string>{ "windows_auth" },
ClientSecrets =
{
new Secret("secret".Sha256())
},
// AllowedScopes = { "api1" }
AllowedScopes = { "api/user" }
}
};
}
internal static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource { Name = "api1",Scopes = new List<Scope> { new Scope { Name = "api1",
DisplayName = "Full access to API 2"} }, Enabled = true, ApiSecrets = new List<Secret>
{
new Secret("secret".Sha256())
}
},
new ApiResource { Name = "api/user",Scopes = new List<Scope> { new Scope { Name = "api/user",
DisplayName = "Full access to API 2"} }, Enabled = true, ApiSecrets = new List<Secret>
{
new Secret("secret".Sha256())
}
}};
}
public static List<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
}
public class WinAuthGrantValidator : IExtensionGrantValidator
{
private readonly HttpContext httpContext;
public string GrantType => "windows_auth";
public WinAuthGrantValidator(IHttpContextAccessor httpContextAccessor)
{
httpContext = httpContextAccessor.HttpContext;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
// see if windows auth has already been requested and succeeded
var result = await httpContext.AuthenticateAsync("Windows");
if (result?.Principal is WindowsPrincipal wp)
{
context.Result = new GrantValidationResult(wp.Identity.Name, GrantType, wp.Claims);
}
else
{
// trigger windows auth
await httpContext.ChallengeAsync("Windows");
context.Result = new GrantValidationResult { IsError = false, Error = null, Subject = null };
}
}
}
}
网络API:Program.cs
public class Program
{
public static void Main(string[] args)
{
var isService = !(Debugger.IsAttached || args.Contains("--console"));
if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);
Directory.SetCurrentDirectory(pathToContentRoot);
}
var builder = CreateWebHostBuilder(
args.Where(arg => arg != "--console").ToArray());
var host = builder.Build();
if (isService)
{
// To run the app without the CustomWebHostService change the
// next line to host.RunAsService();
host.RunAsCustomService();
}
else
{
host.Run();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddEventLog();
})
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.AllowSynchronousIO = true;
options.Authentication.Schemes = Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes.Kerberos | Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes.NTLM;
options.Authentication.AllowAnonymous = true;
options.MaxConnections = null;
options.MaxRequestBodySize = 30000000;
options.UrlPrefixes.Add("http://localhost:5000");
});
}
网络APIUserController.cs
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Bearer")]
[ApiController]
public class UserController : ControllerBase
{
// GET api/user
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { User.Identity.Name, User.Identity.AuthenticationType };
}
}
客户代码:
using (var client = new HttpClient())
{
disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = baseUrl,
Policy = { RequireHttps = false }
});
if (disco.IsError)
{
Console.WriteLine(disco.Error);
Console.ReadLine();
return;
}
var httpHandler = new HttpClientHandler
{
UseDefaultCredentials = true,
};
using (var client = new HttpClient())
{
// request token
TokenResponse tokenResponse = await client.RequestTokenAsync(new TokenRequest
{
GrantType = "windows_auth",
Address = disco.TokenEndpoint,
ClientId = "winauth",
ClientSecret = "secret"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
Console.ReadLine();
return;
}
}
它returns未经授权。
我找到了一个解决方案:我需要用 SubjectId = MYDOMAIN\myusername
配置一个 TestUser 然后它起作用了。
出现的错误过于混乱。