Subsquent HttpWebRequest with NetworkCredential set honors recently changed domain password - httpwebrequest

We gather a windows domain username and password and persist that in a cookie through a web front end. Those credentials along with a request for specific data are on an ongoing basis ultimately passed through a WCF call hosted in windows service that itself redirects that as a new REST call to another server setup under IIS with windows auth configured. This is done using HttpWebRequest...
var url = baseServerUrl + "/" + apiCall.Url;
var request = (HttpWebRequest)WebRequest.Create(url);
request.Timeout = 1000000;
request.Method = apiCall.HttpMethod;
bool credentialsSet = false;
if (!string.IsNullOrEmpty(apiCall.Identity))
{
// split into Domain\User\Password
var parts = apiCall.Identity.Split('\\');
if (parts.Length == 3)
{
request.Credentials = new NetworkCredential(parts[1], parts[2], parts[0]);
credentialsSet = true;
}
}
// Then more code submitting the request ...
So if I store in the cookie domain password A, make a request through the above (which all works fine), but then change my windows domain password to B what we are finding for subsequent requests is the above code (again running as a windows service) will happily authenticate just fine when receiving and setting A on the NetworkCredential (receiving A because the cookie is still hanging around with the old password). If we restart the service immediately any further attempts to use the HttpWebRequest and NetworkCredential with password A generate a 401. Why are the old domain passwords honored until the restart? How do you stop that behavior?

Related

AspNetCore: How to mock external authentication / Microsoft account for integration tests?

I have an OpenID Connect / OAuth2 server (IdP) in my application stack. IdP allows both local and external authentication.
I have integration tests covering most scenarios, but struggle to create a end-to-end test for an external authentication scenario. There are multiple external providers, but from my application perspective they are all using the same workflow over OpenID Connect, only have slight difference (parameters, ie. redirect uri, scheme name, etc.). So it is enough to test one of them. One of them is Microsoft Account (aka. Azure AD)
Integration test is based on WebApplicationFactory (in-memory server with corresponding HttpClient). Local authentication is quite easy, because the whole part runs in my application domain, have access to full source code, etc. I simply create a request to the authorization endpoint and post back user credentials when prompted (I still need to parse the login page to retrieve the antiforgery token, but that's doable)
But when it comes to external, for example Microsoft Account, login involves multiple steps via AJAX and the final post with over 10 parameters, which I unable to reverse engenineer. Other provider has also same level of difficulty.
Since external providers are just blackboxes, from my IdP's perspective, it's just issuing a challenge (redirect to external authorization) and pick up after redirect. Is there a good way to mock the "in between" part?
My solution was to create a middleware, which will mock the external authentication. And then re-configure options for the external authentication scheme to direct to the path middleware is handling. You may also want to overwrite the signingkey (or turn of signature validation). So this code goes to WebApplicationFactory's ConfigureServices/ConfigureTestServices (etc., depending on your setup), to override original setup:
services.AddTransient<IStartupFilter, FakeExternalAuthenticationStartupFilter>();
services.Configure(AuthenticationSchemes.ExternalMicrosoft, (OpenIdConnectOptions options) =>
{
options.Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = FakeExternalAuthenticationStartupFilter.AuthorizeEndpoint,
};
options.TokenValidationParameters.IssuerSigningKey = FakeExternalAuthenticationStartupFilter.SecurityKey;
});
Remark: WebApplicationFactory does not provide a way to override IApplicationBuilder (middleware) stack, so need to add IStartupFilter
The middleware then needs to issue a token with the security key and issue a form post back to the redirect uri. The usual way to achieve this to return simple HTML page with a form which will submit itself once loaded. This works fine in browsers, but HttpClient won't do anything, so the test have to parse the response and create a post request manually.
While this is doable, I wanted to spare this extra step, having to parse respond and re-send it, and make it a single step. Difficulties were:
redirect is not possible (starts as GET request, should ended as POST, need also form data)
cookies issued by OpenIdConnectHandler before redirecting (correlation and nonce) necessary to restore state, only available at redirect uri path (Set-Cookie with path=)
My solution was creating a middleware handling authorization (GET) requests at the same path as the redirect uri is set up, issue token and rewrite request so that OpenIdConnectHandler would pick up. Here's middleware's Invoke method:
public async Task Invoke(HttpContext httpContext)
{
if (!HttpMethods.IsGet(httpContext.Request.Method) || !httpContext.Request.Path.StartsWithSegments(AuthorizeEndpoint))
{
await _next(httpContext);
return;
}
// get and validate query parameters
// Note: these are absolute minimal, might need to add more depending on your flow logic
var clientId = httpContext.Request.Query["client_id"].FirstOrDefault();
var state = httpContext.Request.Query["state"].FirstOrDefault();
var nonce = httpContext.Request.Query["nonce"].FirstOrDefault();
if (clientId is null || state is null || nonce is null)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
var token = CreateToken(clientId, state, nonce); // CreateToken implementation omitted, use same signing key as used above
httpContext.Request.Method = HttpMethods.Post;
httpContext.Request.QueryString = QueryString.Empty;
httpContext.Request.ContentType = "application/x-www-form-urlencoded";
var content = new FormUrlEncodedContent(new Dictionary<string, string>()
{
["id_token"] = token,
["token_type"] = "Bearer",
["expires_in"] = "3600",
["state"] = state,
});
using var buffer = new MemoryStream();
await content.CopyToAsync(buffer, httpContext.RequestAborted);
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
var oldBody = httpContext.Request.Body;
httpContext.Request.Body = buffer;
await _next(httpContext);
httpContext.Request.Body = oldBody;
}

ASP.net Core 2.1 and IdentityServer4 - Client Side and Server Side Cookie Removal

I am working on an Identity Server implementation that makes use of ASP.net Core 2.1 and IdentityServer4 libraries. In the context of OAuth2 protocol, the identity server is implemented in a way to return an AuthorizationCode as soon as the customer provides his/her login credentials through a server provided web-form. The code is returned by the server to a redirectURI that the customer has provided earlier when he first made the login request (see below shown sample login request).
1) EXAMPLE SCENARIO
Sample Login Request:
http://exampleABC.com:5002/connect/authorize?client_id=XYZ&scope=myscope&response_type=code&redirect_uri=http://exampleXYZ.com
Once above like request is issued in browser, the browser opens up a client login page where user is asked to type in his customerid and password. Then, an SMS token page is opened where the customer enters the SMS he has received at his cell phone. The customer then enters the SMS in the browser. Finally, the server redirects the customer's browser to the page at the redirectURI where the browser shows the AuthorizationCode (i.e. code) in the address bar as shown in the following:
https://exampleXYZ.com/?code=89c0cbe1a2cb27c7cd8025b6cc17f6c7cf9bc0d4583c5a63&scope=myscope
Here, the code "89c0cbe1a2cb27c7cd8025b6cc17f6c7cf9bc0d4583c5a63" can be now used to request an AccessToken from the identity server.
2) PROBLEM STATEMENT
If I re-issue the above indicated sample login request in the same client browser (e.g. chrome), then the browser redirects the user to the redirectURI immediately without re-asking the client login credentials. This is a problem because I have to open up a fresh login screen every time the login request is made considering that there can be customers who have different login credentials. Therefore, I have provided a logout endpoint in my IdentityServer implementation where I intend to clean out the entire client cache and then sign out the customer as shown in the following code block. Here, I delete the cookies first and then create a new one with same key and past expiration date in order that the cookie is removed from the client browser cache in addition to the server cache. My aim here is to bring the login web form up-front in the browser at all times with no caching in place if a logout request is issued in order that the login form is displayed every time a new comer customer arrives.
public async Task<IActionResult> Logout()
{
var vm = await BuildLoggedOutView();
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
try
{
if (HttpContext.Request != null && HttpContext.Request.Cookies != null && HttpContext.Request.Cookies.Keys != null && HttpContext.Request.Cookies.Keys.Count > 0)
{
foreach (var key in _accessor.HttpContext.Request.Cookies.Keys)
{
//!!!! Cookie Removal !!!!!!
//Here I delete the cookie first and then recreate it
//with an expiry date having the day before.
_accessor.HttpContext.Response.Cookies.Delete(key);
_accessor.HttpContext.Response.Cookies.Append(
key,
string.Empty,
new CookieOptions()
{
Expires = DateTime.Now.AddDays(-1)
});
}
}
//!!!! Explicit sign out!!!!!!
await _accessor.HttpContext.SignOutAsync();
}
catch (NotSupportedException ex) // this is for the external providers that don't have signout
{
}
catch (InvalidOperationException ex) // this is for Windows/Negotiate
{
}
return View("Logged out", vm);
}
3) QUESTION:
Although I delete the cookies and override them on server side, the client browser keeps returning into the page at redirect uri where a new authorization code is shown without enforcing the customer to login (which is undesired). So, my question here is what am I missing in the above code block? It looks neither cookie override with old expiry date nor the explicit call to SignoutAsync method does not help to sign out the customer completely. Is there some more explicit strategy you might suggest in order to clean out everything both on client and server side completely once logged out?
I've had the same issue with cookies not being deleted properly. In my case it was because I defined a specific path for the authentication cookies. Let's say my path was /path, in that case you have to specify the same path within your delete:
foreach (var cookie in Request.Cookies.Keys)
{
Response.Cookies.Delete(cookie, new CookieOptions()
{
Path = "/path",
// I also added these options, just to be sure it matched my existing cookies
Expires = DateTimeOffset.Now,
Secure = true,
SameSite = SameSiteMode.None,
HttpOnly = true
});
}
Also, I do not know if the .Append() is necessary. By using .Delete() it already sent a set-cookie header in my case.

Google OAuth 2.0 for desktop apps for Windows without Admin privileges

I've heard about Google's plan of modernizing OAuth interactions described here: https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html
Then I was looking at the sample desktop application for Windows found here: https://github.com/googlesamples/oauth-apps-for-windows/tree/master/OAuthDesktopApp.
It's pretty simple and it was working, but once I started Visual Studio without elevated privileges (as a non-admin), I experienced that the HttpListener was not able to start because of the following error: "Access Denied".
It turned out that starting an HttpListener at the loopback address (127.0.0.1) is not possible without admin rights. However trying localhost instead of 127.0.0.1 lead to success.
I found that there is a specific command that allows HttpListener to start at the given address (and port):
netsh http add urlacl url=http://+:80/MyUri user=DOMAIN\user
But it also can be only executed with admin rights, so it's not an option.
Still localhost seems to be the best shot but OAuth 2.0 for Mobile & Desktop Apps states the following regarding this section:
See the redirect_uri parameter definition for more information about the loopback IP address. It is also possible to use localhost in place of the loopback IP, but this may cause issues with client firewalls. Most, but not all, firewalls allow loopback communication.
This is why I'm a bit suspicious to use localhost. So I'm wondering what is the recommended way of Google in this case, as I'm not intending to run our application as administrator just for this reason.
Any ideas?
You can use TcpListener for instance instead of HttpListener. It does not need elevation to listen.
The following is a modified excerpt of this sample:
https://github.com/googlesamples/oauth-apps-for-windows/tree/master/OAuthDesktopApp
// Generates state and PKCE values.
string state = randomDataBase64url(32);
string code_verifier = randomDataBase64url(32);
string code_challenge = base64urlencodeNoPadding(sha256(code_verifier));
const string code_challenge_method = "S256";
// Creates a redirect URI using an available port on the loopback address.
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
string redirectURI = string.Format("http://{0}:{1}/", IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port);
output("redirect URI: " + redirectURI);
// Creates the OAuth 2.0 authorization request.
string authorizationRequest = string.Format("{0}?response_type=code&scope=openid%20profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
authorizationEndpoint,
System.Uri.EscapeDataString(redirectURI),
clientID,
state,
code_challenge,
code_challenge_method);
// Opens request in the browser.
System.Diagnostics.Process.Start(authorizationRequest);
// Waits for the OAuth authorization response.
var client = await listener.AcceptTcpClientAsync();
// Read response.
var response = ReadString(client);
// Brings this app back to the foreground.
this.Activate();
// Sends an HTTP response to the browser.
WriteStringAsync(client, "<html><head><meta http-equiv='refresh' content='10;url=https://google.com'></head><body>Please close this window and return to the app.</body></html>").ContinueWith(t =>
{
client.Dispose();
listener.Stop();
Console.WriteLine("HTTP server stopped.");
});
// TODO: Check the response here to get the authorization code and verify the code challenge
The read and write methods being:
private string ReadString(TcpClient client)
{
var readBuffer = new byte[client.ReceiveBufferSize];
string fullServerReply = null;
using (var inStream = new MemoryStream())
{
var stream = client.GetStream();
while (stream.DataAvailable)
{
var numberOfBytesRead = stream.Read(readBuffer, 0, readBuffer.Length);
if (numberOfBytesRead <= 0)
break;
inStream.Write(readBuffer, 0, numberOfBytesRead);
}
fullServerReply = Encoding.UTF8.GetString(inStream.ToArray());
}
return fullServerReply;
}
private Task WriteStringAsync(TcpClient client, string str)
{
return Task.Run(() =>
{
using (var writer = new StreamWriter(client.GetStream(), Encoding.UTF8))
{
writer.Write("HTTP/1.0 200 OK");
writer.Write(Environment.NewLine);
writer.Write("Content-Type: text/html; charset=UTF-8");
writer.Write(Environment.NewLine);
writer.Write("Content-Length: " + str.Length);
writer.Write(Environment.NewLine);
writer.Write(Environment.NewLine);
writer.Write(str);
}
});
}
By default there is a URL pattern http://+:80/Temporary_Listen_Addresses/ which is allowed for all users (\Everyone)
You can use this as a prefix for your listener. More generally (to avoid collisions with other listeners) you should generate a URL under Temporary_Listen_Addresses (e.g. using a GUID) and use that as your listener prefix.
Unfortunately, a sysadmin can use netsh http to delete this entry or to restrict its usage to only certain users. Also, this does not appear to support listening for an HTTPS request as there is no corresponding ACL entry for port 443.
An admin can list all these permitted URL patterns using netsh http show urlacl as a command.

IdentityServer gets into infinite loop of authentication

I have the following client set up in IdentityServer:
new Client
{
ClientName = "My web application",
Enabled = true,
ClientId = "mywebapp",
ClientSecrets = new List<ClientSecret>
{
new ClientSecret("somesecret")
},
Flow = Flows.Hybrid,
ClientUri = "https://app.mydomain.com",
RedirectUris = new List<string>
{
"oob://localhost/wpfclient",
"http://localhost:2672/",
"https://app.mydomain.com"
}
}
And it is hosted online, let's say https://auth.mydomain.com/core.
Trying to modify the MVC OWIN Client (Hybrid) sample client to log-in to the above identity server, in Startup.cs I modified the ClientId, ClientSecret and RedirectUri to match the client settings in IdSrv. Now when I try to navigate to a page that requires authorization, I am redirected to IdentityServer's URL. When I log-in, the breakpoint hits at AuthorizationCodeReceived notification in the client's Startup.cs and then gets into a loop. The browser's status shows:
Waiting for localhost...
Waitnig for auth.mydomain.com...
Waiting for localhost...
Waitnig for auth.mydomain.com...
...
and so on and never finishes the log-in. Why is this happening? Please help.
Thanks!
Most probably this is caused by mixing http and https in redirects. Please use one scheme consistently and check the scheme on browser address-bar.

Reporting Services Authentication issue

I am trying to programmatically render a PDF using Azure Reporting Services. I suspect that the actual PDF retrieval is fine, but I cannot find a way to authenticate the connection before requesting the report (via URL). I am working in the services layer of my web application and I cannot use a web reference (might not work with Azure) and it doesn't make sense to use a ReportViewer control (since it's a service layer method).
I have all the details to connect, but I suspect that I require a cookie to authenticate and I'm not sure how to manually create this. Any suggestions/solutions?
Here's my code so far:
string userName = BJConfigurationManager.GetSetting("ReportingServiceUsername");
string password = BJConfigurationManager.GetSetting("ReportingServicePassword");
NetworkCredential networkCredential = new NetworkCredential(userName, password);
Domain.Report report = GetReportById(id);
int timeout = 30; //seconds
string url = "https://bleh.ctp.reporting.database.windows.net/ReportServer/Pages/ReportViewer.aspx?...";
string destinationFileName = "#C:\\Temp.pdf";
// Create a web request to the URL
HttpWebRequest MyRequest = (HttpWebRequest)WebRequest.Create(url);
MyRequest.PreAuthenticate = true;
MyRequest.Credentials = networkCredential;
MyRequest.Timeout = timeout * 1000;
try
{
// Get the web response -- THE RESPONSE COMES BACK AS UNAUTHENTICATED...
HttpWebResponse MyResponse = (HttpWebResponse)MyRequest.GetResponse();
Check out the section titled "SOAP Management Endpoint Programmatic Access":
http://msdn.microsoft.com/en-us/library/windowsazure/771e88b6-ab0f-4910-a5fa-5facd8d56767#SOAPManagement.
It explains how to authenticate using a cookie container without a ReportViewer control.
I don't think that is going to work. Azure Reporting uses Forms Authentication and as I understand it, you aren't going to be able to match the Forms Auth cookie along with the MachineKey for encryption.
I was trying to accomplish the same task..but using a WebRequest was impossible.
I changed the approach using a ServerReport class like this:
ServerReport report;
report = new ServerReport();
report.ReportServerUrl = new Uri(reportServerName + "/ReportServer");
report.ReportPath = "/ReportPath";
report.ReportServerCredentials = new ReportServerCredentials();
report.SetParameters(new Microsoft.Reporting.WebForms.ReportParameter("param1", param1));
report.SetParameters(new Microsoft.Reporting.WebForms.ReportParameter("param2", param1));
return report.Render(reportParams.OutputFormat);
The ReportServerCredentials class must implement the IReportServerCredentials interface like this.
More info about the IReportServerCredentials interface and implementation here.