Why are my AJAX requests not extending an OWIN MVC session? - c#

We have an ASP.NET MVC 5 application that has been using Forms Authentication with sliding expiration. We recently switched to OWIN Cookie Authentication and are experiencing issues with our sessions not extending properly.
Previously, a session could be extended from an AJAX xhr request. With this configuration, however, they are not extending. I'm receiving a 200 for every request (both GET and POST) that should be extending, even after the server has killed the session.
The current setup is:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
UrlHelper url = new UrlHelper(HttpContext.Current.Request.RequestContext);
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "Cookies",
CookieSecure = CookieSecureOption.SameAsRequest,
CookieName = Constants.CatalystPortalCookieName,
LoginPath = new PathString(url.Action(nameof(LoginController.Index), "Login")),
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(20),
CookiePath = "/",
});
If I click a link that causes the entire page to be loaded as a document response, however, the server properly extends the session.

I ended up having some faulty assumptions. The AJAX requests were extending. The return of the 200 was throwing me off. After looking with fiddler at the actual response, I saw that with the change to OWIN, the 401 is actually moved in the response:
X-Responded-JSON: {"status":401,"headers":{...foo...}}
So, we ended up just setting the status of the response back to 401. That's probably horrible, but it fit our needs.
protected void Application_EndRequest() {
var context = new HttpContextWrapper(Context);
var header = context.Response.Headers["X-Responded-JSON"];
if (header != null && header.Contains("\"status\":401")) {
Context.Response.Clear();
Context.Response.StatusCode = 401;
}
Here's probably a more robust solution: OWIN: unauthorised webapi call returning login page rather than 401

Related

.NET HttpClient do not persist authentication between reqeusts to IIS when using NTLM Negotiate

IIS site configured to use windows authentication with default options. The client is written in C# and uses single HttpClient instance to perform requests. Requests success, but every request triggers 401 Challenge:
Traffic captured with Wireshark. We performed loud test, and noticed, that with anonymous authentication client performs 5000 reqeusts per second, but with windows authentication - 800. So, looks like wireshark does not impact to authentication, performance slowdown indicates, that 401 Challenge also occurs without wireshark.
Wirehshark log: https://drive.google.com/file/d/1vDNZMjiKPDisFLq6ZDhASQZJJKuN2cpj/view?usp=sharing
Code of client is here:
var httpClientHandler = new HttpClientHandler();
httpClientHandler.UseDefaultCredentials = true;
var httpClient = new HttpClient(httpClientHandler);
while (working)
{
var response = await httpClient.GetAsync(textBoxAddress.Text + "/api/v1/cards/" + cardId);
var content = await response.Content.ReadAsStringAsync();
}
IIS site settings:
How to make HttpClient to persist authentication between requests, to prevent waste negotiate handshakes on every request?
UPD: Code of client: https://github.com/PFight/httpclientauthtest
Steps to reproduce:
Create folder with simple file index.html, create application 'testsite' in IIS for this folder. Enable anonymous authentication.
Run client (https://github.com/PFight/httpclientauthtest/blob/main/TestDv5/bin/Debug/TestDv5.exe), press start button - see count of requests per second. Press stop.
Disable anonymous atuhentication, enable windows authentication.
Press start button in client, see count of reqeusts per second.
On my computer I see ~1000 requests per second on anonymous, and ~180 on windows. Wireshark shows 401 challenges on every request for windows authentication. keep-alive header enabled in IIS.
IIS version: 10.0.18362.1 (windows 10)
Version of System.Net.Http.dll loaded to process: 4.8.3752.0
Firstly I tried to save the Authorization header for re-use it with every new request.
using System.Net.Http.Headers;
Requester requester = new Requester();
await requester.MakeRequest("http://localhost/test.txt");
await Task.Delay(100);
await requester.MakeRequest("http://localhost/test.txt");
class Requester
{
private readonly HttpClientHandler _httpClientHandler;
private readonly HttpClient _httpClient;
private AuthenticationHeaderValue _auth = null;
public Requester()
{
_httpClientHandler = new HttpClientHandler();
_httpClientHandler.UseDefaultCredentials = true;
_httpClient = new HttpClient(_httpClientHandler);
_httpClient.DefaultRequestHeaders.Add("User-Agent", Guid.NewGuid().ToString("D"));
}
public async Task<string> MakeRequest(string url)
{
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, url);
message.Headers.Authorization = _auth;
HttpResponseMessage resp = await _httpClient.SendAsync(message);
_auth = resp.RequestMessage?.Headers?.Authorization;
resp.EnsureSuccessStatusCode();
string responseText = await resp.Content.ReadAsStringAsync();
return responseText;
}
}
But it didn't work. Every time there was http code 401 asking for authentication despite of Authorization header.
The IIS logs is listed below.
2021-12-23 15:07:47 ::1 GET /test.txt - 80 - ::1 c75eeab7-a0ea-4ebd-91a8-21f5cd59c10f - 401 2 5 127
2021-12-23 15:07:47 ::1 GET /test.txt - 80 MicrosoftAccount\account#domain.com ::1 c75eeab7-a0ea-4ebd-91a8-21f5cd59c10f - 200 0 0 4
2021-12-23 15:07:47 ::1 GET /test.txt - 80 - ::1 c75eeab7-a0ea-4ebd-91a8-21f5cd59c10f - 401 1 2148074248 0
2021-12-23 15:07:47 ::1 GET /test.txt - 80 MicrosoftAccount\account#domain.com ::1 c75eeab7-a0ea-4ebd-91a8-21f5cd59c10f - 200 0 0 0
IIS's Failed Requests Tracing reports the following when receiving re-used Authentication Header:
Property
Value
ModuleName
WindowsAuthenticationModule
Notification
AUTHENTICATE_REQUEST
HttpStatus
401
HttpReason
Unauthorized
HttpSubStatus
1
ErrorCode
The token supplied to the function is invalid (0x80090308)
I've made a research and I can say that this is not possible without alive connection.
Every time the connection is closed there will be new handshake.
According this and this answers NTLM authenticates a connection, so you need to keep your connection open.
NTLM over http is using HTTP persistent connection or http keep-alive.
A single connection is created and then kept open for the rest of the session.
If using the same authenticated connection, it is not necessary to send the authentication headers anymore.
This is also the reason why NTLM doesn't work with certain proxy servers that don't support keep-alive connections.
UPDATE:
I found the key point using your example.
First: You must enable keep-alive at your IIS
Second: You must set authPersistSingleRequest flag to false. Setting this flag to True specifies that authentication persists only for a single request on a connection. IIS resets the authentication at the end of each request, and forces re-authentication on the next request of the session. The default value is False.
Third: You can force the HttpClient to send keep-alive headers:
httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
httpClient.DefaultRequestHeaders.Add("Keep-Alive", "600");
Using this three key points I have achieved only one NTLM handshake during connection lifetime.
Also it's important which version of .NET \ .NET Framework do you use.
Because HttpClient hides different realizations dependent on framework version.
Framework
Realization of HttpClient
.Net Framework
Wrapper around WebRequest
.Net Core < 2.1
Native handlers (WinHttpHandler / CurlHandler)
.Net Core >= 2.1
SocketsHttpHandler
I tried it on .NET 6 and it works great, but it didn't work on .Net Framework as I can see, so here is the question: which platform do you use?
UPDATE 2:
Found the solution for .Net Framework.
CredentialCache myCache = new CredentialCache();
WebRequestHandler handler = new WebRequestHandler()
{
UseDefaultCredentials = true,
AllowAutoRedirect = true,
UnsafeAuthenticatedConnectionSharing = true,
Credentials = myCache,
};
var httpClient = new HttpClient(handler);
httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
httpClient.DefaultRequestHeaders.Add("Keep-Alive", "600");
var from = DateTime.Now;
var countPerSecond = 0;
working = true;
while (working)
{
var response = await httpClient.GetAsync(textBoxAddress.Text);
var content = await response.Content.ReadAsStringAsync();
countPerSecond++;
if ((DateTime.Now - from).TotalSeconds >= 1)
{
this.labelRPS.Text = countPerSecond.ToString();
countPerSecond = 0;
from = DateTime.Now;
}
Application.DoEvents();
}
The key point is to use WebRequestHandler with UnsafeAuthenticatedConnectionSharing enabled option and use credential cache.
If this property is set to true, the connection used to retrieve the response remains open after the authentication has been performed. In this case, other requests that have this property set to true may use the connection without re-authenticating. In other words, if a connection has been authenticated for user A, user B may reuse A's connection; user B's request is fulfilled based on the credentials of user A.
Caution
Because it is possible for an application to use the connection without being authenticated, you need to be sure that there is no administrative vulnerability in your system when setting this property to true. If your application sends requests for multiple users (impersonates multiple user accounts) and relies on authentication to protect resources, do not set this property to true unless you use connection groups as described below.
Big thanks to this article for solution.

Redirect for non authorized users not working in ajax request

I have a Cookie based authentication in my webapp to which a sub application make some ajax requests to get the data from db.
the issue is that if the user is not authenticated i redirect him to expired.html, in test mode if i just run in browser or postman an api call like example.com/api/test without getting first the authentication cookie i'm correctly redirected to expired.html. the issue comes when i try to call that api via ajax so by making a simple .get request as the following:
function getPlu(codplu, callback){
let api = 'https://www.example.it/api/plu/?codplu=' + codplu
$.get( api, callback );
}
getPlu('COPERTI', callback => {
...
});
i just get the response from api with code 302 and a .get to expired.html with code 304 but the user still is not redirected to expired.html
So as you can see the status code for that api request is 302 and location should be expired.html BUT it's not getting redirected.
Might it be that browser doesn't handle automatically ajax redirects and i need to do it via client-side (redirect if status.code == 302) or i could fix it via server side?
Here is how the authentication makes the redirect
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => {
options.Cookie.Name = "AUTH_TOKEN";
options.Cookie.MaxAge = TimeSpan.FromMinutes(120);
options.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = (context) =>
{
context.HttpContext.Response.Redirect("https://www.example.it/vmenu/expired.html");
return Task.CompletedTask;
}
};
});
Just to make this answer more clear:
jQuery's ajax uses the XMLHttpRequest object and its methods to execute requests. XMLHttpRequest will follow redirects automatically. Since it's XMLHttpRequest who does that, jQuery's ajax function doesn't even know about it. It only receives the final response, which in the OP's case is 200 Ok (or 304 Not Modified as OP posted).
Also, since the request is made by jQuery/XMLHttpRequest, the client view is not changed if a request or a redirect is executed. Everything is only in the browser's "behind execution".
Since all redirects are executed automatically by XMLHttpRequest, and jQuery is not able to tell if a redirect was made, the most reliable way (and that's the most important thing to me) is handle it manually:
1 - On server side, when unauthenticated request, add a custom header to the response, and respond with 200 OK:
OnRedirectToLogin = (context) =>
{
context.Response.StatusCode = (int)System.Net.HttpStatusCode.OK;
context.Response.Headers.Add("X-Unauthenticated-Redirect", "https://www.example.it/vmenu/expired.html");
return Task.CompletedTask;
}
2 - On client side, just check if this custom header exists. If it does, redirect manually using window.location:
var redirectHeader = jqXHR.getResponseHeader('X-Unauthenticated-Redirect');
if (redirectHeader.length > 0) {
window.location = redirectHeader;
}
Just for reference, from XMLHttpRequest docs:
If the origin of the URL conveyed by the Location header is same origin with the XMLHttpRequest origin and the redirect does not violate infinite loop precautions, transparently follow the redirect while observing the same-origin request event rules.

Azure AD Authentication Breaking HTTP Post Actions When Session Times Out

I recently changed from windows authentication to Azure AD using roughly the "out of the box" code;
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
//AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
AuthenticationContext authContext = new AuthenticationContext(Authority);
return authContext.AcquireTokenByAuthorizationCodeAsync(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
}
}
});
}
Our users have started to get intermittent 404 errors when trying to submit certain forms. I think I have managed to recreate the issue by deleting cookies, so I suspect it's tied to when the session naturally times out.
If I look at the flow with a HTTP GET request it looks like;
HTTP GET https://myappurl/page?param1=value&param2=value
HTTP 302 response with redirect to https://login.microsoftonline.com (including various params; state, client_id etc)
HTTP 200 response (not quite sure how/why it then knows to redirect)
HTTP GET https://myappurl/
HTTP 302 response with redirect to original URL https://myappurl/page?param1=value&param2=value
HTTP GET https://myappurl/page?param1=value&param2=value
HTTP 200 response
Everything works a treat...
For a HTTP POST however;
HTTP POST to https://myappurl/another_page
HTTP 302 response with redirect to https://login.microsoftonline.com (including various params; state, client_id etc)
HTTP 200 response (not quite sure how/why it then knows to redirect)
HTTP GET https://myappurl/
HTTP 302 response with redirect to original URL https://myappurl/another_page
HTTP GET https://myappurl/another_page
HTTP 404 response
Fails because the endpoint only accepts HTTP POST requests.
Any idea if/how I can fix this? I would have thought the built in state tracking or whatever it is doing would store the original request and continue where it left off regardless...
It looks like you are not using the token cache. What this means is that a user's session will expire after about an hour after they sign into the application.
To address this issue you should use AcquireTokenSilentAsync whenever the application needs an access token. This method will automatically refresh the token for you using it's In Memory cache. For more details see https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/wiki/AcquireTokenSilentAsync-using-a-cached-token

WebApi 2 with OWIN - Authorization Challenge does not redirect to Log In

I'm trying to leverage OWIN with OpenIdConnect Authentication to log into Azure Active Directory. My application is ASP .NET using WebApi 2. The front end is just simple HTML/Javascript at this point. When running my application locally I call a WebApi on a controller (neither the controller nor any API's have the [Authorize] filter) and that API in turn uses the current OWIN Context to challenge authentication. This challenge fails as expected because I'm not logged in and proceeds to call "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=..." which is also expected. The request comes back with a 200 status and the response contains the HTML for the Microsoft Log In page. However my web application does not redirect to that log in page and the console displays a CORS error:
Failed to load https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=[removed]&redirect_uri=http%3a%2f%2flocalhost%3a20776%2f&response_mode=form_post&response_type=code+id_token&scope=openid+email+profile+offline_access+User.Read+Mail.Send+Files.ReadWrite&state=OpenIdConnect.AuthenticationProperties[removed]:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:20776' is therefore not allowed access.
I forced my way past the CORS issue with a browser extension but even then it did not redirect to the sign page. Instead the response contained the source for the sign in page as before. I've outlined the steps I've taken and the code I'm using below. Am I missing a step?
Most of the documentation and examples I have found leverage ASP.NET MVC. I've based much of the implementation off of an MVC example found here: https://github.com/Azure-Samples/active-directory-dotnet-webapp-groupclaims
That example runs just fine locally and I'm able to log in with my AAD credentials and successfully call the MS Graph API's.
As for the CORS issue I do not see this in the MVC example. Why is this CORS issue manifesting in my current setup and how do I get around it? I expected the OWIN middleware to take care of this. Using the EnableCors filter on the controller didn't help nor did enabling CORS on IAppBuilder.
The web requests I expect are (based on the OWIN MCV example):
//Call to sign in which initiates the challenge
Request URL:http://localhost:55065/Account/SignIn
Request Method:GET
Status Code:302 Found
Remote Address:[::1]:55065
Referrer Policy:no-referrer-when-downgrade
//Call to perform the challenge. Should redirect to MS login after this
Request
URL:https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=[truncated]
Request Method:GET
Status Code:200 OK
Remote Address:104.42.72.20:443
Referrer Policy:no-referrer-when-downgrade
The web requests I'm actually seeing in my WebApi project are:
Request URL:http://localhost:13291/api/v1/tools/user/signin
Request Method:GET
Status Code:302 Found
Remote Address:[::1]:13291
Referrer Policy:no-referrer-when-downgrade
Request URL:http://localhost:13291/api/v1/tools/user/signin
Request Method:POST
Status Code:302 Found
Remote Address:[::1]:13291
Referrer Policy:no-referrer-when-downgrade
Request URL:https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=[truncated]
Request Method:OPTIONS
Status Code:200 OK
Remote Address:104.210.48.14:443
Referrer Policy:no-referrer-when-downgrade
Request URL:https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=[truncated]
Request Method:GET
Status Code:200 OK
Remote Address:104.210.48.14:443
Referrer Policy:no-referrer-when-downgrade
Here's a summary of my set up followed by relevant code:
Registered a new application through Azure AD with a Reply URL of http://localhost:13291 to match what I'm running locally.
I use that same URL as the RedirectUri for OpenIdConnectionAuthenticationOptions
I use the registered application's AppID as the CliendID for OpenIdConnectionAuthenticationOptions
React is using JQuery $.post
The only place CORS is enabled is off of the IAppBuilder in the OWIN startup class.
Code excerpts
I've removed namespace definitions and some class definitions for brevity. The code below is from a new minimal example I created to reproduce the problem outside of the larger web application I'm working on. The front end is just a simple JQuery call. I'd like to provide access to the solution but it currently contains my AppSecret which I believe is required to even attempt to reproduce this.
WebApiConfig.cs
public static HttpConfiguration Register()
{
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
return config;
}
Startup.cs
[assembly: OwinStartup(typeof(Startup))]
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
Startup.Auth.cs
public partial class Startup
{
//...
//Obtain various application settings
//...
public void ConfigureAuth(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
app.SetDefaultSignInAsAuthenticationType( CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/common/v2.0",
PostLogoutRedirectUri = redirectUri,
RedirectUri = redirectUri, //http://localhost:13291
TokenValidationParameters = ...,
Notifications = ...,
AuthenticationFailed = ...
}
app.UseWebApi(WebApiConfig.Register());
}
}
FooController.cs
public class FooController : ApiController
{
[Route("api/v1/tools/user/signin")]
[HttpPost]
public HttpResponseMessage SignIn()
{
if (HttpContext.Current.User == null || HttpContext.Current.User.Identity.IsAuthenticated == false)
{
var properties = new AuthenticationProperties() {RedirectUri = "/"};
Request.GetOwinContext().Authentication
.Challenge(properties, OpenIdConnectAuthenticationDefaults.AuthenticationType);
return Request.CreateResponse(HttpStatusCode.Unauthorized);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
}
index.html
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript">
function signIn() {
$.post("/api/v1/tools/user/signin", (r) => {
}).fail((e) => {
console.log(e);
});
}
</script>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<input id="signIn" type="button" value="SignIn" onclick="signIn();" />
</body>
</html>
https://weblog.west-wind.com/posts/2016/Sep/26/ASPNET-Core-and-CORS-Gotchas
Check this out, it helped me with cross domain requests from angular on my web api

User authentication when consuming a REST webservice with ServiceStack

The ServiceStack docs are full of examples on how to use server side implementation of authentication of a user. But how does one set the user credentials on the client side?
I use ServiceStack to consume a JSON REST service like this:
var restClient = new JsonServiceClient (baseUri);
var response = restClient.Get<MyResponse> ("/some/service");
How can I add any form of authentication to the request? The webservice I want to consume uses OAuth 1.0, but I am interested in adding custom authentication, too.
In my code, I have previously performed OAuth token exchange successfully, so I already own a valid access token and need to sign every REST request now using this access token and its token_secret.
ServiceStack's AuthTests shows different ways of authenticating when using the ServiceStack Service Clients. By default BasicAuth and DigestAuth is built into the clients, e.g:
var client = new JsonServiceClient(baseUri) {
UserName = UserName,
Password = Password,
};
var request = new Secured { Name = "test" };
var response = client.Send<SecureResponse>(request);
Behind the scenes ServiceStack will attempt to send the request normally but when the request is rejected and challenged by the Server the clients will automatically retry the same request but this time with the Basic/Digest Auth headers.
To skip the extra hop when you know you're accessing a secure service, you can tell the clients to always send the BasicAuth header with:
client.AlwaysSendBasicAuthHeader = true;
The alternative way to Authenticate is to make an explicit call to the Auth service (this requires CredentialsAuthProvider enabled) e.g:
var authResponse = client.Send<AuthResponse>(new Auth {
provider = CredentialsAuthProvider.Name,
UserName = "user",
Password = "p#55word",
RememberMe = true, //important tell client to retain permanent cookies
});
var request = new Secured { Name = "test" };
var response = client.Send<SecureResponse>(request);
After a successful call to the Auth service the client is Authenticated and if RememberMe is set, the client will retain the Session Cookies added by the Server on subsequent requests which is what enables future requests from that client to be authenticated.
Answering myself, as I've found a nice way to do it using the LocalHttpWebRequestFilter hook in the JsonServiceClient:
For securing a web service with OAuth 1.0a, every http request has to send a special Authorization: header. Within this header field, a hash (signature) must be send that uses some characteristics of the request as input data, like the hostname, request url and others.
Now it seems the LocalHttpWebRequestFilter is called by ServiceStack right before the http request is made, and exposes the underlying HttpWebRequest object, where one can add extra headers and access the required fields of the request.
So my solution is now basically:
var client = new JsonServiceClient (baseUri);
client.LocalHttpWebRequestFilter += (request) => {
// compute signature using request and a previously obtained
// access token
string authorization_header = CalculateSignature (request, access_token);
request.Headers.Add ("Authorization", authorization_header);
};
var response = client.Get<MySecuredResponse> ("/my/service");
Note that I use the Devdefined.OAuth library to do all the heavy stuff in CalculateSignature(). The creation of request token, obtaining user authorization, and exchanging the request token for access token as required by OAuth is done outside of ServiceStack, before the above service calls.

Categories