SignalR returning 404 on /negotiate - signalr

I'm using signalR - the following URL usually works:
signalr/negotiate?clientProtocol=1.5&username=bla&connectionData=%5B%7B%22name%22%3A%22apphub%22%7D%5D&_=1427859215639
But when the cookie expires and the user becomes unauthenticated, I start getting 404s on this request. Is there any way to allow requests to /signalr to not require authentication?
My clientside code:
if (app.isAuthenticated()) {
//Start the connection.
window.hubReady = $.connection.hub.start();
}

Related

How to use OkHttp3 to get the generated SessionId for a WebSocket connection?

I need to get SessionId for WebSocket connection. How can I get it?
When my client was on js there were no problems getting it (you can parse socket._transport.url).
A lot of logic is tied to SessionId which cannot be changed already.
If you use SockJS in a browser, you can see this transport URL from which you can subsequently pull out sessionId
wss://host/websocket/052/rs5ahsly/websocket
But this is SockJS, in OkHttp it is probably something else
On the server, I can get sessionId this way, but how to do this on the client?
#MessageMapping("/find")
public void find(SimpMessageHeaderAccessor sha, User user) {
String sessionId = sha.getSessionId();
LOG.debug("New session find - {}, {}", sessionId, user);
// New session find - rs5ahsly, {user: Name, ...}
Repository.add(sessionId, user);
}
Server: spring, stomp
Client: android, Kotlin, stomp client written on top of okhttp3 / WebSocket

How do I get log output from JwtSecurityTokenHandler?

I have an ASP.NET Core 2.1 Web Application project that uses JWT tokens for authenticating the Web API that's built-in to the project. It works fine when I run it locally on my machine, but when I deploy it to Azure (with identical environment and app-settings) it simply returns empty HTTP 401 responses to requests from my authenticated clients and I need to find out why so I can fix it.
I enabled logging of every detail in ASP.NET Core, however I never received any useful output.
First, I added Serilog.AspNetCore and the Console sink to the project through NuGet, then configured logging at Verbose level in Program.cs:
public class Program
{
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.MinimumLevel.Override("Microsoft", LogEventLevel.Verbose)
.MinimumLevel.Override("System", LogEventLevel.Verbose)
.MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Verbose)
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Literate)
.CreateLogger();
CreateWebHostBuilder( args ).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(String[] args)
{
return WebHost.CreateDefaultBuilder( args )
.ConfigureLogging( (ctx, cfg ) =>
{
cfg.ClearProviders();
} )
.UseStartup<Startup>()
.UseSerilog();
}
}
But when I run my web-application on Azure (with console stdout logging to file) I got this output:
[04:13:10 Verbose] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Authorization Filter: Before executing OnAuthorizationAsync on filter
Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter.
[04:13:10 Verbose]
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler
HandleAuthenticateAsync called
[04:13:10 Debug]
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler
AuthenticationScheme: Bearer was not authenticated.
[04:13:10 Information]
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService
Authorization failed.
[04:13:10 Verbose] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Authorization Filter: After executing OnAuthorizationAsync on filter
Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter.
[04:13:10 Information]
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
[04:13:10 Verbose] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Before executing action result Microsoft.AspNetCore.Mvc.ChallengeResult.
[04:13:10 Information] Microsoft.AspNetCore.Mvc.ChallengeResult
Executing ChallengeResult with authentication schemes (["Bearer"]).
[04:13:10 Verbose]
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler
Forwarding challenge to scheme: BearerIdentityServerAuthenticationJwt
Note how despite verbose logging, the error messages (repeated below) don't give me any explanation:
AuthenticationScheme: Bearer was not authenticated.
Authorization failed.
I dug around the ASP.NET Core Security source-code to see that JwtBearerHandler.HandleAuthenticateAsync doesn't do much logging of its own, but it does call into the not-open-sourced System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler which does normally do a lot of logging, including detailed reasons (e.g. (with IDX10209-type error codes in strings), but I don't know why it isn't outputting anything I can capture.
How do I log messages from JwtSecurityTokenHandler?
I found the problem:
My HttpClient (that was sending the HTTP Authorization header Bearer token) was unintentionally sending it to a http:// URI that immediately received a 301 redirect to a https:// URI. The redirect was performed by IIS without the ASP.NET Core pipeline getting involved.
The HttpClient class does not re-send the Authorization header following a redirect (this is by-design).
I never noticed this because my HttpClient's received HttpResponseMessage had a reference to the original request which had the Authorization header, not the post-redirect request that lacked the header. I had to use Fiddler with the HTTPS proxy to see the second request was lacking the Authorization header.
When IdentityServerAuthenticationHandler or ASP.NET Core's own JwtBearerHandler receives a request with no Authorization header it does not call into JwtSecurityTokenHandler at all. To see this, open the JwtBearerHandler.cs file in the ASP.NET Core Security Git repo and look at HandleAuthenticateAsync: It has this logic:
if (string.IsNullOrEmpty(token))
{
string authorization = Request.Headers["Authorization"];
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
So in my case, it never actually called JwtSecurityTokenHandler at all, hence the lack of output messages about JWT validation.
However the output messages I did receive did not help. They're both misleading:
"AuthenticationScheme: Bearer was not authenticated." should have been something like "AuthenticationScheme: No Bearer token was present in the request." instead.
And "Authorization failed." should have been "Authorization skipped because no token was present in the request."
So in the end, the fix was to change the original request URI's scheme from http:// to https://.

The anti-forgery cookie token and form field token do not match when using WebApi

I have a single-page app (user loads a bunch of HTML/JS and then makes AJAX requests without another call to MVC - only via WebAPI). In WebAPI I have the following:
public sealed class WebApiValidateAntiForgeryTokenAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(
System.Web.Http.Controllers.HttpActionContext actionContext)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
if (actionContext.Request.Method.Method == "POST")
{
string requestUri = actionContext.Request.RequestUri.AbsoluteUri.ToLower();
if (uriExclusions.All(s => !requestUri.Contains(s, StringComparison.OrdinalIgnoreCase))) // place some exclusions here if needed
{
HttpRequestHeaders headers = actionContext.Request.Headers;
CookieState tokenCookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName]) // __RequestVerificationToken
.FirstOrDefault();
string tokenHeader = string.Empty;
if (headers.Contains("X-XSRF-Token"))
{
tokenHeader = headers.GetValues("X-XSRF-Token").FirstOrDefault();
}
AntiForgery.Validate(!string.IsNullOrEmpty(tokenCookie?.Value) ? tokenCookie.Value : null, tokenHeader);
}
}
base.OnActionExecuting(actionContext); // this is where it throws
}
}
Registered in Global.asax:
private static void RegisterWebApiFilters(HttpFilterCollection filters)
{
filters.Add(new WebApiValidateAntiForgeryTokenAttribute());
filters.Add(new AddCustomHeaderFilter());
}
Occasionally, I see the The anti-forgery cookie token and form field token do not match error in my logs. When this is happening, both tokenCookie.value and tokenHeader are not null.
Clientside, all of my AJAX requests use the following:
beforeSend: function (request) {
request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
},
With Razor generating the token once on my SPA page:
#Html.AntiForgeryToken()
I have my machine key set in Web.config.
What could be causing this?
Update
I just checked logs and I'm seeing this sometimes as well:
The provided anti-forgery token was meant for user "", but the current user is "someuser#domain.com". a few seconds ago
This occurs when a user refreshes their instance of the SPA while logged in. The SPA then drops them into the landing page instead of the inner page for some reason (User.Identity.IsAuthenticated is true) - then they can't log in because of this error. Refreshing pulls them back inside. Not sure what this means, but I figured more info can't hurt.
Appendix
https://security.stackexchange.com/questions/167064/is-csrf-protection-useless-with-ajax/167076#167076
My answer will recommend to not try to use CSRF protections based on tokens in AJAX calls, but rather to rely on the native CORS features of the web browser.
Basically, any AJAX call from the browser to the back-end server will check for the domain origin (aka the domain where the script was loaded from). If the domains match (JS hosting domain == target AJAX server domain) the AJAX calls performs fine, otherwise returns null.
If an attacker tries to host a malicious AJAX query on his own server it will fail if your back-end server has no CORS policy allowing him to do so (which is the case by default).
So, natively, CSRF protections are useless in AJAX calls, and you can lower your technical debt by simply not trying to handle that.
More info on CORS - Mozilla Foundation
Code example - use your console inspector!
<html>
<script>
function reqListener () {
console.log(this.responseText);
}
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.open("GET", "http://www.reuters.com/");
oReq.send();
</script>
</html>
Run it and look at the Security error:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading
the remote resource at http://www.reuters.com/. (Reason: CORS header
‘Access-Control-Allow-Origin’ missing).
Mozilla is pretty clear regarding the Cross-site XMLHttpRequest implementation:
Modern browsers support cross-site requests by implementing the Web
Applications (WebApps) Working Group's Access Control for Cross-Site
Requests standard.
As long as the server is configured to allow requests from your web
application's origin, XMLHttpRequest will work. Otherwise, an
INVALID_ACCESS_ERR exception is thrown.
I try to give an answer the same, also if in the comments we exchange, yours it seems a not related scenario with mine..
A such type of issue can be due to the XMLHttpRequest.setRequestHeader() behaviour, because this function "combines" the values of an header that has been already assigned in the context of an http request, as stated by MDN and Whatwg:
If this method is called several times with the same header, the
values are merged into one single request header.
So, if we have a SPA for example that executes all ajax POSTs setting a given http header, in your case:
beforeSend: function (request) {
request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
}
the first ajax POST request sets a clear header ("X-XSRF-Token") and so, server side, you should have a "valid" header value to compare to.
But, in absence of a page refresh, or a new GET request, all subsequent ajax POSTs, as well as stated in the MDN and Whatwg documentation, will make a dirty assignment of the same header ("X-XSRF-Token"), because they combine the new values with the olds.
To avoid this issue, you could try to reset "X-XSRF-Token" value (but there isn't much documentation on that and it seems a not reliable solution...)
beforeSend: function (request) {
request.setRequestHeader("X-XSRF-Token", null); //depends on user agents..
//OR.. request.setRequestHeader("X-XSRF-Token", ''); //other user agents..
//OR.. request.setRequestHeader("X-XSRF-Token"); //other user agents..
request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
}
Other solutions can rely on some client-side state handing mechanism that you have to implement on your own, because it is not possible to get values or state access of the http request headers (only response headers can be accessed).
Update - revision of the following text:
So, if we have a SPA for example that executes all ajax POSTs recycling the XMLHttpRequest object for each calling and setting a given http header, in your case:
...

SignalR cross-domain connections with self-hosting and authentication

I have a CORS problem when self-hosting SignalR with OWIN, which only happens when I try to enable authentication.
The error I get in my web browser is:
XMLHttpRequest cannot load http://.../signalr/negotiate?[snip] Origin ... is not allowed by Access-Control-Allow-Origin
This only happens if I enable authentication in my self-hosted server using the approach in this answer:
public void Configuration(IAppBuilder app)
{
var listener = (HttpListener)app.Properties[typeof(HttpListener).FullName];
listener.AuthenticationSchemes = AuthenticationSchemes.Ntlm;
app.MapHubs(new HubConfiguration { EnableCrossDomain = true });
}
If I comment out the AuthenticationSchemes line then CORS works (and I've checked everything in these instructions). I get the same problem if I use other authentication schemes than NTLM.
Using Fiddler to examine what's going on, without authentication enabled I see the necessary CORS headers coming back from the server:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: [my server]
However once I enable authentication I get a 401 response which is missing these headers. All the requests have the necessary Origin header.
Having examined the SignalR source code it looks like the headers are being set, but presumably with authentication enabled the HttpListener is sending the initial 401 response without hitting this code.
So I think my question is: How do I get the HttpListener to include an Access-Control-Allow-Origin header in its negotiation of authentication protocols?
I have gotten NTLM authentication to work with cross domain signalR self-hosted in OWIN by allowing the preflight requests anonymous access.
What one needs to do is create a delegate for choosing the authentication scheme which looks for the preflight request headers, and allows these through anonymously. All other requests will use NTLM.
public void Configuration(IAppBuilder appBuilder)
{
var listener = (HttpListener)appBuilder.Properties[typeof(HttpListener).FullName];
listener.AuthenticationSchemeSelectorDelegate += AuthenticationSchemeSelectorDelegate;
}
private AuthenticationSchemes AuthenticationSchemeSelectorDelegate(HttpListenerRequest httpRequest)
{
if (httpRequest.Headers.Get("Access-Control-Request-Method")!=null)
return AuthenticationSchemes.Anonymous;
else
return AuthenticationSchemes.Ntlm;
}
I presume you're using Chrome, which very unhelpfully tells you that these headers are missing and that this is the problem, when actually you have probably just forgot to set your XMLHttpRequest's withCredentials property to true.
If you're using jQuery you can do this for all requests with:
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
options.xhrFields = { withCredentials: true };
});
You also need to do the right thing with OPTIONS requests as in the other answer.

Forms Authentication and POST requests from AJAX

We have an ASP.NET app protected by forms authentication. The app uses MS AJAX heavily to call its web-services.
When the forms authentication times out, and a GET-request happens - all is fine (the user is redirected to a login page).
BUT when the forms authentication times out and a POST-request happens (ajax) - no redirect happens, instead the app returns "401 unathorized" and the browser prompts for username and password (not a login form, but a browsers built-in dialog). Of course entering ANY username/password never helps.
How do I handle this?
UPDATE: After looking with firebug, I just found out that regular POST requests redirect to login fine, it's only web-service calls that throw "401 Unauthorizes".
The difference between a regular request and web-service is URL. Which is "page.aspx" for a regular post-request and "service.asmx/MethodName" for webservices...
Ok, answering my own questin.
After looking into this issue and researching a bit more I found that when a web-app is protected by Forms-Authentication and the user is not authenticated, this is what happens:
If it's a GET-request - the user is
redirected to the login page.
If it's a POST-request to a page - the user is
redirected to the login page.
If it's a POST-request to a web-service - the
user gets 401-unauthorized
Thats how ASP.NET works
And if a web-service is called by AJAX (xmlHttpRequest object) and returns 401 - of course the browser shows a pop-up login box.
Now, what should you do is add some code to Application_PostAuthenticateRequest that will prevent throwing 401 for webservices.
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
if (Request.RequestType == "POST" //if its POST
&& !User.Identity.IsAuthenticated //if user NOT authed
&& !HasAnonymousAccess(Context) //if it's not the login page
)
{
//lets get the auth type
Configuration config = WebConfigurationManager.OpenWebConfiguration("~");
SystemWebSectionGroup grp = (SystemWebSectionGroup)config.GetSectionGroup("system.web");
AuthenticationSection auth = grp.Authentication;
//if it FORMS auth
if(auth.Mode== AuthenticationMode.Forms)
{
//then redirect... this redirect won't work for AJAX cause xmlHttpRequest can't handle redirects, but anyway...
Response.Redirect(FormsAuthentication.LoginUrl, true);
Response.End();
}
}
}
public static bool HasAnonymousAccess(HttpContext context)
{
return UrlAuthorizationModule.CheckUrlAccessForPrincipal(
context.Request.Path,
new GenericPrincipal(new GenericIdentity(string.Empty), null),
context.Request.HttpMethod);
}
I see two solutions:
(1) "Heart beat" mechanism. On each page include a script that will "ping" the server by some dummy ajax request, like:
<script>
setInterval(ping, 60000); // based on comment by John
function ping()
{
$.get('/do/nothing');
}
</script>
This way the session shouldn't expire as long as the browser window is open.
(2) On each ajax request check the status of the response. If the response has "401 unauthorized" code (or any other code different that 200), that means that the session expired and instead of loading the response into some dialog box in the page redirect the user to login page.
Conclusion based on comments:
The best solution would be to combine the two above mechanisms. Heartbeat mechanism will help to keep the session alive as long as the page is displayed in the browser. But in doesn't guarantee that for sure. The connection to the server can be broke and reopened when the session is expired. So you should check the response status anyway.

Resources