NodeJS and AngularJS - Secure REST API with Client Certificate Authentication - angularjs

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?

Related

Identity Server Invalid Request Because Of Http Redirection

I am using .net core mvc as identity server client application. When going to the identity server in localhost, the redirect uri goes as https and connects, no problem. But it goes as http on the test server. And cause it is defined as https on the identity server, invalid request returns. How do I make this http part https?
Eror picture:
https://ibb.co/2k2LK5j
I am using UserHttpsRedirection()
I marked with ** related parts in requests.
Working request:
https://test.identityserversite.com/im/connect/authorize?client_id=bla_bla_client&redirect_uri=**https**://localhost:5001/signin-oidc&response_type=id_token
token&scope=openid
profile&response_mode=form_post&nonce=637975263254748013.MTUyNTc5OTgtYmJlMS00ZTAyLTgxMjAtNTNjZDNmZDBjMjY3MWIyNzg2ZDEtNmE1OS00NTUyLWEwNWMtNDI1N2ZiODAwMGVk&state=CfDJ8N9E0C7WvvpEoty-MgpSP4cRET_Y6sOuFDy58PjxCQcyD64gal_CBwXNx6DbTf7FyF8sQ9sJxeGZH1dAQPRn6mVHvUULG3FFz99XW7O9fpq8lTRvDSSxmBMoyBmSt4KwjbXdE60mbnllnlC7kbWT1ytqArRRKj8YtzkGQp2eg69TbuTJkgwIJsnFFbsfZ0Uo0A9xdYBP7eQRuMi9HrCZ6RU8l28E-U4fByg5Qzss2dcmLJDhPxWJd94z17MJwgEK6d1L34Kivc33NsGhXtwvFSypp6m2sgkSR3fT_bwvH-yy&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=6.10.0.0
Not Working request:
https://test.identityserversite.com/im/connect/authorize?client_id=bla_bla_client&redirect_uri=**http**://clienttestsite.com/signin-oidc&response_type=id_token
token&scope=openid
profile&response_mode=form_post&nonce=637975265040994314.NmNiY2Q1MWQtOGJmYy00OWE0LTgxNGMtODk5MjkyY2QwOGNhYTJkMzdlZmItMzI4Ny00NmFkLTg3YjgtYjA4M2UzZDQ5OGZj&state=CfDJ8BWH8w1S3EBHujbHFc1L6rvNMx0jXRaUdB5aDFJ5wMA4IF5h17dNCV78tPAPLThXL6lS937Rz6mt3Jrbhn1cjozAeIL4bFu5YRkQLQeBKdGeAYA2Ikh610MqPSrG7bnCezbpdKrsGVNIKYZqIBuECh_gEm45T_b5HcuhzucF2du1Cz8sDtDmDzYKuSjBUo49b4-YNxM1zkGH8v2dkWxhNpduYYMQJwV53yy_BogGgaaT_8i9bffFKl_rYfOgtNAiw2OzZRpEqaqdvjCNQEetaNPcNnZiJYBEnloBSeG73njLAHIEoWD-OveWAT5-216OBw&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=6.10.0.0
Error Update:
We must use server as http without ssl behind the load balancer by company policy.
We have conf page like this:
options.Authority = identitySettings.Issuer;
options.ClientId = identitySettings.ClientId;
options.ClientSecret = identitySettings.ClientSecret;
options.RequireHttpsMetadata = true;
options.UsePkce = true;
options.SaveTokens = true;
options.TokenValidationParameters.NameClaimType = "given_name";
options.TokenValidationParameters.RoleClaimType = "role";
options.ClaimActions.DeleteClaim("s_hash");
//options.SignedOutCallbackPath = "/account/accessdenied";
//options.SignedOutRedirectUri = "/account/accessdenied";
Func<RedirectContext, Task> redirectToIdentityProvider = (ctx) =>
{
if (!ctx.ProtocolMessage.RedirectUri.StartsWith("https"))
ctx.ProtocolMessage.RedirectUri = ctx.ProtocolMessage.RedirectUri.Replace("http", "https");
return Task.FromResult(0);
};
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = redirectToIdentityProvider
};
// this code changes http redirectUri to https redirectUri
options.CorrelationCookie.SameSite = SameSiteMode.None;
options.NonceCookie.SameSite = SameSiteMode.None;
//options.TokenValidationParameters.ValidateIssuer = false;
//options.TokenValidationParameters.ValidAudience = "9cd9c695-cd02-4b70-a10b-df669cd77ed8";
options.ResponseType = "id_token token";
options.Scope.Clear();
var scopes = identitySettings.Scopes.Split(',');
foreach (var scope in scopes)
{
options.Scope.Add(scope);
}
When we send request from browser our site redirects to identity server with http(issue at top). Identity server wants us to come with https. First identity server return us to "invalid reqquest". Cause allowedRedirectUri is "https:localhost:5001" So we manipulate the redirectUri http to https.(with event OnRedirectToIdentityProvider . You can see on top code). Now our site returns "correlation failed".
UPDATE
Type of our problem has been changed in a while. We figured out problen at the top with assigning ssl to server, not only load balancer.
You should not try to use OpenID connect over HTTP using a browser. Why? Besides security, the browsers will not accept the cookies involved when sent over HTTP.
This is due to the samesite=none attribute set on some of the cookies. Browsers will reject these cookies if sent over HTTP.
HTTP problem
your client seems in development since it is on local host. the client will deploy as http service behind reverse proxy and the domain/subdomains is configured with ssl in the front as your identity is referred as https address. (SSL offloading)
https://test.identityserversite.com
so i dont think you have any problem in that. my services including identity server are http behind reverse proxy and i have subdomain with ssl configuration in nginx and i dont have any problem. and ofcourse for dployment you configure forward headers.
Corelation
in my case it happens when i played around samesite cookies.the configurations for cookie are mismatch between identity server and the client. i was able to reach login but after login i got the error.but it seems you stay samesite to none i guess. how ever it is mostly related to you cookies between client and identityserver

PassportJS not deserializing user on Heroku server

I'm working on a project which is using PassportJS Google OAuth 2.0. When I test on my local machine (with a React client on localhost:3000 and a Express server on localhost:4000), the flow works fine. I am able to send requests to the server and deserialize the user on each request. However, when I host the client on Google Firebase Hosting and the server on Heroku, the user no longer get deserialized on each request.
Here are some specifics of the things I've done / tried / worked locally along with extra information:
The client and server and hosted on different domains.
I am using axios to send the request to the server. In the request, I make sure to set the "withCredentials" property in the options to true to make sure the cookies connected to that domain are sent on each request.
On the server I have CORS enabled for the domain the client is hosted on (as it is currently being hosted on a different domain) and I have "credentials" set to true to allow the credentials to be sent and received.
Please let me know if I've forgotten to include something in the post or if any extra information would be helpful. Thank you in advance.
I don't know if you fix this, but I got the exact same problem, in my case I added sameSite: "none" in my express session setting, it worked.
cookie: {
sameSite: "none", //add this line
...
},

React SignalR - Do not send bearer token in URL

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"
}
}
}

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