React SignalR - Do not send bearer token in URL - reactjs

I am writing an application which has just failed pen testing for the following:
Authorisation Token is being sent in the URL:
https://domain/Hub?access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSIsImtpZCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSJ9.....
This is happening automatically when sending to the Hub which uses Azure AD authorisation.
constructor (hub: string) {
this.hubName = hub;
this.hub = new HubConnectionBuilder()
.configureLogging(LogLevel.Critical)
.withUrl(`${this.hubURL}${hub}` , {
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
accessTokenFactory: () => {
return `${getToken()}`
}
})
.build();
}
I've scoured for documentation, however I was wondering if there was a way to connect and send requests without exposing the bearer token within the URL?

From the documentation
When using WebSockets or Server-Sent Events, the browser client sends
the access token in the query string. Receiving the access token via
query string is generally secure as using the standard Authorization
header. Always use HTTPS to ensure a secure end-to-end connection
between the client and the server. Many web servers log the URL for
each request, including the query string. Logging the URLs may log the
access token. ASP.NET Core logs the URL for each request by default,
which will include the query string. For example:
And from this documentation
In standard web APIs, bearer tokens are sent in an HTTP header.
However, SignalR is unable to set these headers in browsers when using
some transports. When using WebSockets and Server-Sent Events, the
token is transmitted as a query string parameter.
Seems to me you could disable WebSockets and Server-Sent events. See this question on how to remove WebSockets or Server-Sent events. But then you fallback to long polling or forever frame, and you may not want that.
Since your URL in your question is https I wouldn't bother that much if you have disabled the request logging.
Changing the log level for Microsoft.AspNetCore.Hosting could be done in your appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore.Hosting": "Warning"
}
}
}

Related

NodeJS and AngularJS - Secure REST API with Client Certificate Authentication

I am currently working on making my REST Api Server (NodeJS + Express + Mongoose) secure, so nobody, except my client application (AngularJS 1.6) and my admin application (based on AngularJS 1.6), can access the routes and fetch or put data into my database. Everything is running on https with a valid SSL certificate.
I mainly thought about two approaches:
Shared secret keys where specific routes needs an "access key"
Client certificate authentication
I went with no. 2, because in my thoughts this is the most secure (please correct if I am wrong :))
So I set up my API Server to run on https and request a valid client certificate:
var options = {
ca: fs.readFileSync(__dirname + "/cert/server.ca"),
key: fs.readFileSync(__dirname + "/cert/server.key"),
cert: fs.readFileSync(__dirname + "/cert/server.crt"),
requestCert: true,
rejectUnauthorized: false
};
https.createServer(options, app)
.listen(PORT, () => {
console.log(`up and running #: ${os.hostname()} on port: ${PORT}`);
console.log(`enviroment: ${process.env.NODE_ENV}`);
});
I handle the rejection of unathorized users directly in the app via:
if (!req.client.authorized) {
var cert = req.socket.getPeerCertificate();
console.log("unauthorized: ", cert);
return res.status(401).send('Not authorized!');
}
And here the problems begin :). On every request my client application does I receive an error:
401 - not authorized
I thought that the client application is sending the SSL certificate with every request (or if requested by nodejs) via "requestCert" and everything is working just fine. But it seems to be a bit more complicated.
In my server.ca file I currently have the certificate chain which I received from the CA.
In console.log the transmitted certificate in the request, but its always empty.
What am I doing wrong? Do I have to configure Angular to send it along with every request? Any suggestions?

MVC with Angular - ValidateAntiForgeryToken fails after Azure Login redirect with Adal

I have an MVC site with an embedded angular client and I've recently implemented an anti forgery XSRF token as a security measure.
I have set it up in Startup.cs as follows:
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
app.Use(next => context =>
{
if (string.Equals(context.Request.Path.Value, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.Request.Path.Value, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// We can send the request token as a JavaScript-readable cookie, and Angular will use it by default.
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
return next(context);
});
And I've implemented it within my angular front-end like so:
{ provide: XSRFStrategy, useFactory: xsrfFactory}
export function xsrfFactory(): CookieXSRFStrategy {
return new CookieXSRFStrategy('XSRF-TOKEN', 'X-XSRF-TOKEN');
}
And protecting my controllers like:
[Authorize] //Validation of AzureAD Bearer Token.
[ValidateAntiForgeryToken]
public class UserController : Controller
It is intended that the X-XSRF-TOKEN header be validated with any call to my API, and this works successfully for all calls in the original session. However, my app uses Adal to log the user in, and after the redirect from a successful login, this validation step fails and I receive a 400 from my API for any subsequent calls.
The original X-XSRF-TOKEN header is still sent with all outgoing requests from my angular client after the login so I suspect it must be that my server side no longer has the token to validate against, or my server has generated a new one and my client doesn't retrieve it. But for whatever reason it breaks down and it's very hard to debug without creating some custom filter so I can see what's going on inside it.
Is there a way to reset this token after a client side redirect so that both my server and client share common knowledge of it again? Or should I be generating the token in my Index.html for example?
EDIT
Edited controller decoration above for missing [Authorize] attribute.
So my controller is protected by a step validating the AzureAD Bearer token as well as the Anti-Forgery validation. Removing the AzureAD Validation as a test did not resolve the issue, oddly.
Error on failing API calls displays in output after Adal login as:
The provided anti-forgery token was meant for a different claims-based user than the current user.
Based on my understanding, you were protecting the controller using token. For this issue is expected, you can refer the progress of validate the anti-XSRF tokens from below(refer this link):
To validate the incoming anti-XSRF tokens, the developer includes a ValidateAntiForgeryToken attribute on her MVC action or controller, or she calls #AntiForgery.Validate() from her Razor page. The runtime will perform the following steps:
The incoming session token and field token are read and the anti-XSRF token extracted from each. The anti-XSRF tokens must be identical per step (2) in the generation routine.
If the current user is authenticated, her username is compared with the username stored in the field token. The usernames must match.
If an IAntiForgeryAdditionalDataProvider is configured, the runtime calls its ValidateAdditionalData method. The method must return the Boolean value true.
Since you were developing the SPA application with back-end web API, when the request to the web API, it will always issue the anti-XSRF token with no identity. And when you send the request to the back-end with anti-XSRF and Azure AD token, this time the web API already authenticate the request via the Azure AD token. And it will always return false when checking anti-XSRF token to match the identity information.
In this scenario, if the back-end only using the bear token authentication and store the token with session storage, there is no need to enable XSRF prevention since others is not able to steal the token and forge the request.
If your back-end also support the cookie authentication or basic auth, NTLM etc, you can disable the identity checking by adding the following to your Application_Start method: AntiForgeryConfig.SuppressIdentityHeuristicChecks = true.(refer this link)
More detail about XSRF/CSRF abouth oauth and web API, you can refer the threads below:
How does ValidateAntiForgeryToken fit with Web APIs that can be accessed via web or native app?
AntiForgeryToken does not work well with OAuth via WebAPI
Try replacing [ValidateAntiForgeryToken] with [AutoValidateAntiforgeryToken]
https://github.com/aspnet/Antiforgery/blob/dev/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs

Include cookies in HTTP requests when using the Google Cloud Endpoints JavaScript client

I am currently making API calls to my backend using the Google Cloud Endpoint generated JavaScript Client. The problem is the cookies for my page are not being added to the HTTP requests. How can I add the Gitkit gtoken cookie to my request.
Backend is Google App Engine Java
Using Goole Cloud Endpoints to build my API
Using the Google Cloud Endpoints JavaScript web client loaded as follows gapi.client.load('myApi', 'v1', resourceLoaded, 'https://my-project-id.appspot.com/_ah/api');
I have already configured Google Cloud Endpoints, on the backend, to allow cookies. auth = #ApiAuth(allowCookieAuth = AnnotationBoolean.TRUE)
My endpoint looks as follows.
#ApiMethod(path = "user-account")
public UserAccount get(HttpServletRequest httpRequest) {
GitkitUser gitkitUser = Gitkit.validate(httpRequest); // returns null
Cookie[] cookies = httpRequest.getCookies();
log.severe("# of cookies: " + cookies.length);
for (Cookie cookie : cookies) {
log.severe("cookie name: " + cookie.getName());
log.severe("cookie value: " + cookie.getValue());
}
/*
* Logs 1 for # of cookies, with a cookie name of "G_ENABLED_IDPS"
* a value of "google". No gtoken cookie, even though I have
* checked and there is one!
*/
...
}
I am making calls with the Google Cloud Endpoints JS client as so.
gapi.client.myApi.userAccountResource.get().execute(function (resp){
...
});
Is there something I have to do to make sure the Endpoints JS client includes the gtoken cookie in it's request?
You better add screenshots of cookies storage + request headers and create a plunker/jsfiddle/jsbin to reproduce the problem.
There are chances that cookies are not set or not send to server. You need to localize where is a problem. If it's sent over wire by browser then issue is on server side. If it's in cookies storage but not sent it's client issue. If it's not in storage there is just nothing to sent and it's a different problem to find out why they are not at client at all.
You can view cookies & requests headers in devtools of your browser. And yes, cookies are send automatically if not expired and match to host & path prefix.

AngularJS: Sending Authentication Headers over Websocket

I am having difficulty authenticating requests to a WebSocket API.
The Site I am working with (www.bitmex.com) provides a REST API and a WebSocket API.
Both of their API's allow authentication with an API Key.
Authentication Requirements
The API provides the following documentation for authentication with API Keys:
Authentication is done by sending the following HTTP headers:
api-expires: a UNIX timestamp in the future (eg: 5 seconds).
api-key: Your public API key. This the id param returned when you create an API Key via the API.
api-signature: A signature of the request you are making. It is calculated as hex(HMAC_SHA256(verb + url + nonce + data)).
REST API
I've created a NodeJS module for sending requests to the REST API, I've defined the following headers
headers = {
"User-Agent": "BitMEX NodeJS API Client",
"api-expires": expires,
"api-key": this.api_key,
"api-signature": this.signMessage(verb, reqUrl, expires, params)
};
where the signMessage function looks like:
BitMEX.prototype.signMessage = function signMessage(verb, url, nonce, data) {
if (!data || _.isEmpty(data)) data = '';
else if(_.isObject(data)) data = formatParameters(data);
return crypto.createHmac('sha256', this.secret).update(verb + url + nonce + data).digest('hex');
};
This works great for the REST API and does everything I need it to in the backend of my application.
WebSocket API
I am trying to use WebSocket get realtime data and display it in a browser based interface.
The documentation on the site states:
To use an API Key with websockets, you must sign the initial upgrade request in the same manner you would sign other REST calls.
I've been implementing this in AngularJS using the ng-websocket module.
exchange.dataStream = $websocket('wss://testnet.bitmex.com/realtime');
exchange.dataStream.onMessage(function incoming (message) {
console.log("BitMEX: WS MESSAGE RECEIVED: " + message.data);
// .. handle data here ...
});
exchange.dataStream.send({"op":"getAccount"});
The problem that I've run into is I can't find anyway to send the headers using ng-websocket that are needed for authentication.
If I am presently logged in to BitMEX from another tab in my browser, this will connect, get the data, and work as expected.
However, if I am not currently logged in to the site, it will throw the following error:
BitMEX: WS MESSAGE RECEIVED: {"status":401,"error":"Not authenticated.","meta":{},"request":{"op":"getAccount"}}
There is a python example provided here: https://github.com/BitMEX/market-maker/blob/master/test/websocket-apikey-auth-test.py that goes through the Authentication process,
but I haven't found a way to accomplish this in AngularJS.
Summary
#1) When logged in to BitMEX, and the Websocket is working, is Chrome somehow using the website's cookies to authenticate the websocket requests?
Looking at an overview of websockets here: http://enterprisewebbook.com/ch8_websockets.html
The initial handshake upgrades the connection from "HTTP" to the WebSocket protocol,
#2) Because this initial connection is over HTTP, is there any way to attach the headers required to this initial HTTP request?
If you read the Python example, the first thing it sends is {"op": "authKey", "args": [API_KEY, nonce, signature]} then it sends {"op": "getAccount"}
Python example line #44 and line #51
How Chrome does it is another question.

How can I add a spring security JSESSIONID with SockJS and STOMP when doing a cross-domain request?

I am having the following problem. I will describe 3 use cases - two which work and the other one which doesn't.
I have an AngularJS client using SockJS with STOMP. In the backend I have a Spring application. Client is in domain domainA.com, backend is in domainB.com.
var socket = new SockJS(("http://domainB.com/myApp/messages"));
stompClient = Stomp.over(socket);
stompClient.connect('guest', 'guest', function(frame) {
...
}
In the backend there are Cors filters and the Cross-origin calls are possible. All works fine.
Use Case 1. Client domainA, Server domainB
My application is unsecured on the backend side. I subscribe like below:
stompClient.subscribe('/topic/listen', function(message) {
showMessage(JSON.parse(message.body).content);
});
All works fine.
Use Case 2. Client domainB, Server domainB
My application is secured on the backend side with Spring security. Authentication is done through a form - username and password. Nothing uncommon.
In this case the client is on domainB.com, same as the backend. All works fine, the difference is that I use a different subscription method:
stompClient.subscribe('/user/queue/listen', function(message) {
showMessage(JSON.parse(message.body).content);
});
in order to benefit from getting the principal from the security session. All works well. No issues.
The JSESSIONID cookie is added to the connection request in new SockJS(("http://domainB.com/myApp/messages"));.
Use Case 3. Client domainA, Server domainB
The application is secured the same as in UC2. However, the client is now on a different domain.
The JSESSIONID is NOT added to the connection request. The connection to websocket in Spring is unauthenticated and redirected back to login page. This repeats and makes it impossible to connect.
Why is the JSESSIONID cookie not populated with the websocket requests in this case?
Cheers
Adam
As part of SockJS protocol, a http GET is sent to websocket server for negotiating the supported protocols. It's done using XmlHttpRequest which won't add any cookies stored for a different domain than its own domain the web application and scripts are served due to same-origin policy implemented in every modern web browser.
You should resort to a way of circumventing the same-origin policy.
I think you'll find the answers you are looking for here : http://spring.io/blog/2014/09/16/preview-spring-security-websocket-support-sessions
the trick to implement a HandshakeInterceptor

Resources