-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WindowsIdentity.RunImpersonated sending wrong credentials #58033
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
This is a bit of a trap. RunImpersonated Takes a .NET 5 added RunImersonatedAsync to mitigate this issue. Try that and see if it helps. |
This sounds like HttpClient failing to account for the impersonation context when selecting pooled connections. Transferring back to runtime for followup. |
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
Tagging subscribers to this area: @dotnet/ncl Issue DetailsHi all! I'm experiencing the problem described in issue #38414. I have a ASP.NET Core app with a Middleware, that authenticate user via windows authentication and runs under his account an http-request to get a jwt-token. Startup.cs public class Startup
{
//...
//omitted for brevity
//...
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(conf => conf.Cookie.MaxAge = TimeSpan.FromMinutes(30));
services.AddAuthentication(IISDefaults.AuthenticationScheme).AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = _configuration.GetValue<string>("IdentityServer");
options.SupportedTokens = SupportedTokens.Both;
options.ApiSecret = _configuration.GetValue<string>("ClientSecret");
options.RequireHttpsMetadata = false;
});
services.AddHttpClient(ServiceConsts.ImpersonatedHttpClientName)
.ConfigurePrimaryHttpMessageHandler(_ =>
new SocketsHttpHandler()
{
UseProxy = false,
Credentials = CredentialCache.DefaultCredentials
});
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
//...
//omitted for brevity
//...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.UseForwardedHeaders();
app.Use(async (context, next) =>
{
string ip = context.Connection.RemoteIpAddress.ToString();
context.Request.Headers[ForwardedIpToken] = ip;
await next.Invoke();
});
app.UseMiddleware<WinTokenMiddleware>();
app.UseOcelot().Wait();
}
//...
//omitted for brevity
//...
} WinTokenMiddleware.cs [SupportedOSPlatform("windows")]
public class WinTokenMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private readonly IHttpClientFactory _httpClientFactory;
private static readonly ILogger Logger = Log.ForContext<WinTokenMiddleware>();
private const string TokenSessionKey = "AuthToken";
public WinTokenMiddleware(RequestDelegate next, IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
_next = next;
_configuration = configuration;
_httpClientFactory = httpClientFactory;
}
// ReSharper disable once UnusedMember.Global
public async Task InvokeAsync(HttpContext context)
{
//...
//... omitted for brevity
//...
AuthenticateResult winAuthenticateResult = await context.AuthenticateAsync(IISServerDefaults.AuthenticationScheme);
if (!winAuthenticateResult.Succeeded)
{
await context.ChallengeAsync(IISDefaults.AuthenticationScheme);
return;
}
if (!(winAuthenticateResult?.Principal?.Identity is WindowsIdentity windowsIdentity))
{
Logger.Warning("Couldn't get user windows account!'");
await WriteErrorResponse(context,
"Couldn't get user windows account! Please enable windows authentication on server.");
return;
}
//...
//... omitted for brevity
//...
TokenResponse tokenResponse = await WindowsIdentity.RunImpersonated(
windowsIdentity.AccessToken,
() =>
{
Logger.Debug($"Current user {WindowsIdentity.GetCurrent().Name}");
HttpClient impersonatedClient = _httpClientFactory.CreateClient(ServiceConsts.ImpersonatedHttpClientName);
//получаем токен под пользователем
return impersonatedClient.RequestTokenAsync(new TokenRequest()
{
Address = disco.TokenEndpoint,
ClientId = _configuration.GetValue<string>("ClientId"),
ClientSecret = _configuration.GetValue<string>("ClientSecret"),
GrantType = "windows"
});
});
//...
//... omitted for brevity
//...
}
} The Code above gets jwt-Token authenticated under user account with windows authentication. If I set PooledConnectionLifetime to TimeSpan.Zero, it works as expected services.AddHttpClient(ServiceConsts.ImpersonatedHttpClientName)
.ConfigurePrimaryHttpMessageHandler(_ =>
new SocketsHttpHandler()
{
UseProxy = false,
Credentials = CredentialCache.DefaultCredentials,
PreAuthenticate = false,
PooledConnectionLifetime = TimeSpan.Zero
}); The service is running on net5.0 on windows server 2016, IIS 10. Is this a bug or am I doing it the wrong way?
|
Thanks for reply! But I've tried this TokenResponse tokenResponse = await WindowsIdentity.RunImpersonatedAsync(
windowsIdentity.AccessToken,
async () =>
{
Logger.Debug($"Current user {WindowsIdentity.GetCurrent().Name}");
HttpClient impersonatedClient = _httpClientFactory.CreateClient(ServiceConsts.ImpersonatedHttpClientName);
return await impersonatedClient.RequestTokenAsync(new TokenRequest()
{
Address = disco.TokenEndpoint,
ClientId = _configuration.GetValue<string>("ClientId"),
ClientSecret = _configuration.GetValue<string>("ClientSecret"),
GrantType = "windows"
});
}); Startup.cs services.AddHttpClient(ServiceConsts.ImpersonatedHttpClientName)
.ConfigurePrimaryHttpMessageHandler(_ =>
new SocketsHttpHandler()
{
UseProxy = false,
Credentials = CredentialCache.DefaultCredentials
}); Issue is still present. When I open page under first account I receive correct token, when I open page under different account after that, I receive token of previous user. |
Can you confirm the issue goes away if you create a new HttpClient instance for each call to RequestTokenAsync (without the factory) and dispose it afterwards? That should help confirm if it's a connection pooling issue. |
Yes, issue goes away if I create a new HttpClient instance for each call to RequestTokenAsync. It also goes away when I set up HttpClient not to use connection pooling like this services.AddHttpClient(ServiceConsts.ImpersonatedHttpClientName)
.ConfigurePrimaryHttpMessageHandler(_ =>
new SocketsHttpHandler()
{
UseProxy = false,
Credentials = CredentialCache.DefaultCredentials,
PooledConnectionLifetime = TimeSpan.Zero
}); |
Just checked, this works TokenResponse tokenResponse = await WindowsIdentity.RunImpersonatedAsync(
windowsIdentity.AccessToken,
async () =>
{
Logger.Debug($"Current user {WindowsIdentity.GetCurrent().Name}");
using HttpClient impersonatedClient = new HttpClient(new HttpClientHandler(){UseDefaultCredentials = true});
//получаем токен под пользователем
return await impersonatedClient.RequestTokenAsync(new TokenRequest()
{
Address = disco.TokenEndpoint,
ClientId = _configuration.GetValue<string>("ClientId"),
ClientSecret = _configuration.GetValue<string>("ClientSecret"),
GrantType = "windows"
});
}); But it isn't optimal solution, because it could lead to socket exhaustion. |
We have code to handle this here: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs#L281 Sounds like something is broken somewhere, though... |
@alnikola Can you take a look at this? |
In particular, these two lines seem wrong: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs#L78 We only seem to set these properties in the constructor, so when the credentials are set this won't get updated properly. @alnikola Does that seem right to you? How do we test this functionality? Are there manual tests? When do we run them? |
Triage: @alnikola does not have the context (discussed offline). |
@daniilzaonegin will you be able to validate that it solved your problem on 6.0 RC2 daily builds? Thanks! |
@karelz, just installed VS2022 Pre on my desktop, I'll try to check if the problem is still there later today |
@daniilzaonegin I don't think 6.0 RC2 is yet part of any VS install, you would need to install 6.0 RC2 daily build from https://github.com/dotnet/core/blob/main/daily-builds.md#future-releases (they are marked as 6.0 daily builds) as it is not officially released yet. |
@karelz, ok, I'll install it, thank you! |
Hello @karelz !
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ocelot" Version="17.0.0" />
<PackageReference Include="Ocelot.Administration" Version="17.0.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="System.Security.Principal.Windows" Version="6.0.0-preview.5.21301.5" />
</ItemGroup>
</Project> When I run it using dotnet run , I get an error
[SupportedOSPlatform("windows")]
public class WinTokenMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private readonly IHttpClientFactory _httpClientFactory;
private static readonly ILogger Logger = Log.ForContext<WinTokenMiddleware>();
private const string TokenSessionKey = "AuthToken";
public WinTokenMiddleware(RequestDelegate next, IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
_next = next;
_configuration = configuration;
_httpClientFactory = httpClientFactory;
}
// ReSharper disable once UnusedMember.Global
public async Task InvokeAsync(HttpContext context)
{
Logger.Information("User is not authenticated by jwt token. Authenticating user using windows authentication.");
try
{
AuthenticateResult winAuthenticateResult = await context.AuthenticateAsync(IISServerDefaults.AuthenticationScheme);
if (!winAuthenticateResult.Succeeded)
{
await context.ChallengeAsync(IISDefaults.AuthenticationScheme); //<--The error happens here
return;
}
// ------ omitted for brevity ----------
}
} And when I run it on IIS Express, the issue is still there. |
@daniilzaonegin it does seem like failure to set up a working test case, not sign that it is the same issue. Or am I reading it incorrectly? |
In essence, the old code would run as wrong user but it would not fail to run. |
@karelz, maybe we better wait till dotnet 6 will be released? It is only one week left. Then I can definitely check it. |
@daniilzaonegin that is fine with me. Would be great to have final confirmation that we truly addressed the end-to-end scenario, given that this is 2nd attempt to fix it. |
@daniilzaonegin 6.0 is now GA. If you could check your scenario, that would be helpful for us. Thanks! |
Hi @karelz! I tried to reproduce it today and the bug is still there. <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ocelot" Version="17.0.0" />
<PackageReference Include="Ocelot.Administration" Version="17.0.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="System.Security.Principal.Windows" Version="6.0.0-preview.5.21301.5" />
</ItemGroup>
</Project> Startup.cs: using IdentityServer4.AccessTokenValidation;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.IISIntegration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Hosting;
public class Startup
{
private readonly IConfiguration _configuration;
private const string ForwardedIpToken = "X-Forwarded-For";
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(conf => conf.Cookie.MaxAge = TimeSpan.FromMinutes(30));
services.AddAuthentication(IISDefaults.AuthenticationScheme).AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = _configuration.GetValue<string>("IdentityServer");
options.SupportedTokens = SupportedTokens.Both;
options.ApiSecret = _configuration.GetValue<string>("ClientSecret");
options.RequireHttpsMetadata = false;
});
services.AddHttpClient(ServiceConsts.IdentityHttpClientName,
c => c.Timeout = Timeout.InfiniteTimeSpan)
.ConfigurePrimaryHttpMessageHandler(_ =>
new HttpClientHandler()
{
UseProxy = false,
UseDefaultCredentials = true
});
services.AddHttpClient(ServiceConsts.ImpersonatedHttpClientName)
.ConfigurePrimaryHttpMessageHandler(_ =>
new HttpClientHandler()
{
UseProxy = false,
UseDefaultCredentials = true
});
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// Включаем чтение Header от всех источников, вне зависимости от подсети
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
services.AddOcelot();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.UseForwardedHeaders();
app.Use(async (context, next) =>
{
string ip = context.Connection.RemoteIpAddress.ToString();
//выставляем ip-адрес пользователя для сервисов-потребителей
context.Request.Headers[ForwardedIpToken] = ip;
await next.Invoke();
});
app.UseMiddleware<WinTokenMiddleware>();
app.UseOcelot().Wait();
}
} WinTokenMiddleware [SupportedOSPlatform("windows")]
public class WinTokenMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private readonly IHttpClientFactory _httpClientFactory;
private static readonly ILogger Logger = Log.ForContext<WinTokenMiddleware>();
private const string TokenSessionKey = "AuthToken";
public WinTokenMiddleware(RequestDelegate next, IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
_next = next;
_configuration = configuration;
_httpClientFactory = httpClientFactory;
}
// ReSharper disable once UnusedMember.Global
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Method == "OPTIONS")
{
//OPTIONS requests not needed to be authorized
await _next.Invoke(context);
return;
}
//try to get token from session
string sessionToken = context.Session.GetString(TokenSessionKey);
if (sessionToken != null)
UpdateOrAddAuthHeader(context, sessionToken);
AuthenticateResult authResult =
await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
context.User = authResult.Principal;
if (context.User.Identity.IsAuthenticated)
{
//jwt-token is ok, no need to get it
Logger.Information($"Request is authorized by jwt token in it.");
await _next.Invoke(context);
return;
}
Logger.Information("User is not authenticated by jwt token. Authenticating user using windows authentication.");
try
{
AuthenticateResult winAuthenticateResult = await context.AuthenticateAsync(IISServerDefaults.AuthenticationScheme);
if (!winAuthenticateResult.Succeeded)
{
//user is not authenticated by windows, making challenge request
await context.ChallengeAsync(IISDefaults.AuthenticationScheme);
return;
}
if (!(winAuthenticateResult?.Principal?.Identity is WindowsIdentity windowsIdentity))
{
//user is not authenticated after challenge request - Error
//stop processing
Logger.Warning("Couldn't get user windows account!'");
await WriteErrorResponse(context,
"Couldn't get user windows account! Please enable windows authentication on server.");
return;
}
//user is successfully authenticated using windows auth.
Logger.Debug($"Current windows user:'{WindowsIdentity.GetCurrent().Name}'");
Logger.Information("Discovering token end point...");
HttpClient httpClient =
_httpClientFactory.CreateClient(ServiceConsts.IdentityHttpClientName);
DiscoveryDocumentResponse disco = await httpClient.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = _configuration.GetValue<string>("IdentityServer"),
Policy =
{
RequireHttps = false
}
});
if (disco.IsError)
{
//token end-point not found stop processing, we can't get jwt-token
Logger.Error($"Indentity server discovery error:{disco.Error}");
await WriteErrorResponse(context,
$"Indentity server discovery error:{disco.Error}");
return;
}
Logger.Information($"Token end point found '{disco.TokenEndpoint}'. Requesting token...");
TokenResponse tokenResponse = await WindowsIdentity.RunImpersonated(
windowsIdentity.AccessToken,
() =>
{
Logger.Debug($"Current user {WindowsIdentity.GetCurrent().Name}");
HttpClient impersonatedClient = _httpClientFactory.CreateClient(ServiceConsts.ImpersonatedHttpClientName);
//making call of token-endpoint using user windows credentials
return impersonatedClient.RequestTokenAsync(new TokenRequest()
{
Address = disco.TokenEndpoint,
ClientId = _configuration.GetValue<string>("ClientId"),
ClientSecret = _configuration.GetValue<string>("ClientSecret"),
GrantType = "windows"
});
});
if (tokenResponse.IsError)
{
Logger.Warning($"Token getting error:{tokenResponse.Error}");
await WriteErrorResponse(context, $"Token getting error:{ tokenResponse.Error}");
return;
}
Logger.Information($"Got Token! Setting authorization header...");
UpdateOrAddAuthHeader(context, tokenResponse.AccessToken);
Logger.Information($"Storing Token to session for future use.");
context.Session.SetString(TokenSessionKey, tokenResponse.AccessToken);
}
catch (Exception ex)
{
Logger.Error(ex, "Exception occured during WinToken generation.");
await WriteErrorResponse(context, "Exception occured during WinToken generation.");
return;
}
await _next(context);
}
private static async Task WriteErrorResponse(HttpContext context, string message)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(message);
}
private void UpdateOrAddAuthHeader(HttpContext context, string token)
{
if (context.Request.Headers.ContainsKey("Authorization") &&
!context.Request.Headers["Authorization"]
.Contains(JwtBearerDefaults.AuthenticationScheme, StringComparer.OrdinalIgnoreCase))
{
//we update the header only if it doesn't contain Jwt-token
context.Request.Headers["Authorization"] =
JwtBearerDefaults.AuthenticationScheme + " " + token;
}
else
{
context.Request.Headers.Add("Authorization",
JwtBearerDefaults.AuthenticationScheme + " " + token);
}
}
}
} First call using credetials of account RAIFFEISEN\admzed1
User is correct, see above on line 21.
User is incorrect. See above line 28. |
Note you should be using the new RunInpersonatedAsync API so the impersonation isn't reverted before the async operation completes. |
Ok, I will try it! Thanks |
Hi, @Tratcher! I changed the code of WinTokenMiddleware and called TokenResponse tokenResponse = await WindowsIdentity.RunImpersonatedAsync(
windowsIdentity.AccessToken,
() =>
{
Logger.Debug($"Current user {WindowsIdentity.GetCurrent().Name}");
HttpClient impersonatedClient = _httpClientFactory.CreateClient(ServiceConsts.ImpersonatedHttpClientName);
//получаем токен под пользователем
return impersonatedClient.RequestTokenAsync(new TokenRequest()
{
Address = disco.TokenEndpoint,
ClientId = _configuration.GetValue<string>("ClientId"),
ClientSecret = _configuration.GetValue<string>("ClientSecret"),
GrantType = "windows"
});
}); But the issue is still there. First call under user RAIFFEISEN\ruazed1. Line 23 correct login:
First call under user RAIFFEISEN\admzed1. Line 28 incorrect login:
|
I don't know, maybe there an error in my code somewhere? |
@karelz , I've checked my scenario on dotnet 6. |
Thanks @daniilzaonegin! Would you mind opening a new bug -- and referencing this one? |
Ok, I'll do it in one or two days. I'll try to create a minimal solution to check it. |
To close the loop: It works as expected after all - see new issue discussion: #63136 (comment) |
Hi all!
I'm experiencing the problem described in issue #38414.
I have a ASP.NET Core app with a Middleware, that authenticate user via windows authentication and runs under his account an http-request to get a jwt-token.
Startup.cs
WinTokenMiddleware.cs
The Code above gets jwt-Token authenticated under user account with windows authentication.
When we deploy application, we see that users get tokens of other users. For example, if user A gets authenticated correctly and received his token, and few seconds later comes user B, he gets authenticated by user A and therefore gets the wrong token.
If I set PooledConnectionLifetime to TimeSpan.Zero, it works as expected
The service is running on net5.0 on windows server 2016, IIS 10.
Is this a bug or am I doing it the wrong way?
The text was updated successfully, but these errors were encountered: