Cookie Initially Set with Secure Attribute loses Secure Attribute - security

I have non-session authentication cookies that have an expiration of 2 weeks. They are initially set with the Secure attribute. At some point these cookies are losing the Secure attribute.
Could the browser be doing this? When the cookie is first set, Secure is set and SameSite = "None". When the Secure attribute is lost, SameSite is then set to "Lax". So far, I've only noticed this happening at least two days after creating the cookie. I have noticed this in Firefox and Chrome.
I need to start setting SameSite = "Lax", so I'm going to start testing this.
More info:
MVC 3 Web App using FormsAuthentication
Login is the only way to get an auth cookie
I create a FormsAuthenticationTicket, encrypt the ticket, then use the encrypted ticket when calling
new HttpCookie("myAuthCookieName", encryptedFormsAuthenticationTicket);
Web.config is set to require Secure Cookies:
<httpCookies httpOnlyCookies="true" requireSSL="true" />

Related

How does owin middleware track change in cookie

How does owin middleware track change in cookie? I am using cookie authentication. After cookie is generated and attached to browser if I make any change in cookie by using F12(developer toolbar in browser) and send request to sever how is owin validating cookie?
var cookieAuthenticationOptions = new CookieAuthenticationOptions
{
CookieName = "test",
AuthenticationType = "Cookies",
ExpireTimeSpan = TimeSpan.FromHours(12),
SlidingExpiration = false
};
app.UseCookieAuthentication(cookieAuthenticationOptions);
PREFERRED BEHAVIOR
Auth cookies are strongly encrypted since they contain tokens in some cases. The pattern for auth cookies is to use properties such as these:
HTTP Only
Encrypted
SameSite=strict
Domain=web origin
Secure
Symmetric authenticated encryption is used, with an algorithm such as AES256-GCM. This means any edit to any cookie byte will fail decryption, resulting in a 401 error. You should test this.
Cookies are time restricted also. Either the exp claim of stored tokens is checked on every request, or a separate timestamp within the cookie data.
OWIN
Sone notes here on OWIN crypto. It is quite an old tech now, so does not use the most cutying edge encryption algorithms. Instead it uses machine key based encryption, which I believe uses AES + HMACSHA under the hood. The end result is essentially the same behavior as above though.

Cookie for a refresh-token is not stored

I'm currently struggling with JWT and refresh tokens. I implemented the following flow from the OAuth 2 spec.
The user gets a JWT token which is used for requests and before it expires with the refresh token a new JWT is retrieved.
It works fine, the refresh token is properly returned by the API, but not stored in the cookies storage of the browser. The request for obtaining the token are using { withCredentials: true }
The way how the user gets the refresh token looks like the following. The web app runs on example1.com and the API server on example2.com, both run with https. Is there a error in my cookie configuration or some other mistake in my code?
const cookieOptions = {
httpOnly: true,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
secure: true,
sameSite: 'none',
};
res.cookie('refresh_token', token, cookieOptions);
Thanks in advance!
The cookie will be considered third party and browsers drop these aggressively. Aim to convert this to a first party cookie instead, via these steps:
Web app origin is https://www.example.com
API runs at https://api.example.com - a sibling domain
Cookie domain is .example.com
This kind of domain relationship is a prerequisite to using secure cookies these days, due to recent browser restrictions to prevent tracking. I expect your code will work fine once the domain setup is correct.
For further info see this Curity Code Example and the use of a hosts file to test the domain setup. This example also encrypts the cookie using AES256 and uses the cookie setting SameSite=strict.

CSRF protection - is a JWT and CORS whitelist combination sufficient?

I'm working to address CSRF vulnerabilities in my React/Phoenix app, and it seems to me like my app is safe... but I'm not an expert in these matters, and wanted to turn to the community to see if I've overlooked something or am being naïve.
The Phoenix is a pure API, running separately from the React client, so I'm dealing with CORS - the whitelist of allowed origins is set in the Phoenix router.ex:
pipeline :api do
plug CORSPlug, [origin: "localhost:3000"]
plug :accepts, ["json"]
plug Guardian.Plug.VerifyHeader, realm: "Bearer"
plug Guardian.Plug.LoadResource
end
and, as you can see, I'm using Guardian (which uses JWT for user authentication) for handling authorization.
Authorized clients store the JWT in localStorage, and Guardian is set to look for that value in the Authorization header of requests as a Bearer... protected Phoenix controllers include:
plug Guardian.Plug.EnsureAuthenticated
I set up a test attacker, running on localhost:5000 to try to mock a CSRF attack. First, I tried an AJAX attack - I copied a valid JWT value from the localStorage of a logged-in window and set it in the request header of my mock attacker. As expected, this fails because localhost:5000 is not whitelisted
The 'Access-Control-Allow-Origin' header contains the invalid value 'null'. Origin 'http://localhost:5000' is therefore not allowed access.
To test, I added localhost:5000 to the Phoenix whitelist and the request did indeed work... so it seems that, even if an attacker managed to steal a valid JWT, they'd be stopped by the whitelist.
I then tested an automated form submission, borrowed from the OWASP docs:
<body onload='document.CSRF.submit()'>
<form action='http://localhost:4000/api/v1/user' method='POST' name='CSRF'>
<input type='hidden' name='name' value='Hacked'>
<input type='hidden' name='password' value='Hacked'>
</form>
<body>
but this gets caught by Guardian.Plug.EnsureAuthenticated, set in the API's controllers, since there is no Authorization header nor valid JWT present:
[info] POST /api/v1/user
[debug] Processing with MyApp.UserController.create/2
Parameters: %{"name" => "Hacked", "password" => "[FILTERED]"}
Pipelines: [:api]
[info] Sent 401 in 21ms
[debug] MyApp.UserController halted in
Guardian.Plug.EnsureAuthenticated.call/2
So my impression is that AJAX attacks will fail, even with a valid JWT, because of the CORS whitelist... simple requests will fail because they don't include the Authorization header.
I've been reading a lot about CSRF protection when using JWTs for authorization, but it seems like no two people can agree on what is and isn't safe. Am I missing something, or is the combination of the CORS whitelist and Guardian JWT check sufficient to protect agains CSRF?
You mention 2 methods for CSRF prevention: 1) CORS & 2) JWT stored in localStorage and associated to the user's session. I would like to address both:
1) CORS does help prevent certain types of CSRF attacks by preventing attempts from non-origin sources from making HTTP requests on the user's behalf. This prevents GETs/POSTs from external sources, good job. It will not prevent CSRF attacks from internal sources. So on to #2...
2) JWT stored in localStorage passed thru Authentication/Bearer header can help but is always still susceptible to XSS attacks. XSS prevention is critical. Now the JWT is accessible via javascript and can be passed with any request. To help, the website should prevent javascript access to the token. The recommended approach is to store the JWT in an HTTPOnly cookie. This cookie is in addition to the Authentication/Bearer header. On the Phoenix server side, the first authorization step the API needs to do is ensure that the JWT from the HTTPOnly cookie is the same as the Authentication/Bearer header. Only then can the API be successfully invoked.

Invalid state on azure, but working locally

I have an Azure Active Directory tenant that I wish to authenticate with from my Node.js application running on an Azure App Service instance. I'm using passportjs and passport-azure-ad to do this.
Locally everything works fine. I can authenticate with the Azure AD tenant and it returns back to my page correctly. However on Azure it fails with the error:
authentication failed due to: In collectInfoFromReq: invalid state received in the request
My configuration is exactly the same (apart from redirectUrl) as I'm using the same tenant for local testing as well as in Azure yet it still fails. I've set up the proper reply urls and the authentication returns back to my application.
Here is my config:
{
identityMetadata: `https://login.microsoftonline.com/${tenantId}/.well-known/openid-configuration`,
clientID: `${clientId}`,
responseType: 'id_token',
responseMode: 'form_post',
redirectUrl: 'https://localhost:3000/auth/oidc/return',
allowHttpForRedirectUrl: false,
scope: [ 'openid' ],
isB2C: false,
passReqToCallback: true,
loggingLevel: 'info'
}
I'm using the OIDCStrategy.
My authentication middleware:
passport.authenticate('azuread-openidconnect', {
response: res,
failureRedirect: '/auth/error',
customState: '/'
});
I've compared the encoded state on the authorizerequest vs the returned response and they differ in the same way locally as well as on Azure, yet Azure is the only one complaining. Examples of how the states differ:
Azure:
Request state: CUSTOMEwAuZcY7VypgbKQlwlUHwyO18lnzaYGt%20
Response state: CUSTOMEwAuZcY7VypgbKQlwlUHwyO18lnzaYGt
localhost:
Request state: CUSTOMTAYOz2pBQt332oKkJDGqRKs_wAo90Pny%2F
Response state: CUSTOMTAYOz2pBQt332oKkJDGqRKs_wAo90Pny/
I've also tried removing customState completely yet it still fails.
Anyone know what's going on here? Am I configuring it incorrectly?
Edit: It appears that this may not be an issue with passport-azure-ad. I'm not sure yet, but some debugging revealed that there is no set-cookie header on the login request to my app. The session is created, but no cookie is set thus the returning response is unable to look up the session info including the state and compare them. The result is that it reports invalid state since it's unable to retrieve data from the session.
Turns out the problem was that the session was never properly created thus there was no state for process-azure-ad to compare. The reason for this was that I had configured express-session to use secure session cookies under the assumption that since I was connecting through the https://...azurewebsites.net address the connection was secure. This is not technically the case though.
Azure runs a load balancer in front of the Web Application effectively proxying connections from the outside to my app. This proxy is where the secure connection is terminated and then traffic is routed unencrypted to my application.
Browser -(HTTPS)> Load balancer -(HTTP)> Application
The result is that node did not report the connection as secure unless a set the configuration option trust proxy:
app.set('trust proxy', true);
When this option is set express will check the X-Forwarded-Proto header for which protocol was used to connect to the proxy server (in this case the load balancer). This header contains either http or https depending on the connection protocol.
For Azure though this is still not sufficient. The Azure load balancer does not set the X-Forwarded-Proto header either. Instead it uses x-arr-ssl. This is not a big problem though as iisnode (the runtime I'm using to run node on IIS in Azure) has an option called enableXFF that will update the X-Forwarded-Proto header based on the external protocol of the connection. Setting both these options enables express-session to set the secure cookie keeping the session stored and allowing passport-azure-ad to store and compare state information on authentication.
PS: Big thanks to Scott Smiths blog + comments for providing the answer:
http://scottksmith.com/blog/2014/08/22/using-secure-cookies-in-node-on-azure/
This is a known encode issue with module passport-azure-ad. See:
"State" gets encoded and causes "collectInfoFromReq: invalid state received" #309
"invalid state received in the request" causing infinite loop on Login #247
You could upgrade the module version to v3.0.7 or a newer one to fix it.

Owin WS-Federation setting up token sliding expiration

Can somebody explain how to implement sliding expiration using the new Owin WS-Federation plugin?
On the client side, at WS-Fedeartion configuration I see that there are some events like :
Notifications = new WsFederationAuthenticationNotifications
{
SecurityTokenReceived = ...,
AuthenticationFailed = ...,
RedirectToIdentityProvider = ...,
MessageReceived = ...,
SecurityTokenValidated = ....
},
But because the lack of documentation I can't really figure it out where an how?
At the moment my STS is issuing tokens with absolute expiration:
protected override Lifetime GetTokenLifetime(Lifetime requestLifetime)
{
// 5 Minutes for token lifetime
var lifetime = new Lifetime(DateTime.UtcNow, DateTime.UtcNow.AddMinutes(5));
return lifetime;
}
Any help is higly appreciated.
TL;DR: set WsFederationAuthenticationOptions.UseTokenLifetime to false, to re-enable sliding expiration.
In OWIN/Katana, the sliding expiration concept is limited to the cookies middleware and is enabled by default (you can turn it off by setting CookieAuthenticationOptions.SlidingExpiration to false: https://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.Cookies/CookieAuthenticationOptions.cs).
When you use app.UseWsFederationAuthentication (or app.UseOpenIdConnectAuthentication), it actually relies on another middleware to persist the ClaimsIdentity when you complete the authentication flow. This "persistence delegation" can be configured through the SignInAsAuthenticationType or via app.SetDefaultSignInAsAuthenticationType.
Typically, this SignInAsAuthenticationType property corresponds to a cookie middleware: this way, sliding expiration is not managed at the WS-Federation middleware level, but by the cookies middleware, that will automatically renew the authentication cookie when sliding expiration conditions are met. In this scenario, the authentication token issued by your identity provider won't be renewed. For this to work, you need to set WsFederationAuthenticationOptions.UseTokenLifetime to false, because when you use the default value, sliding expiration is disabled and the cookie lifetime matches the token lifetime.
If you use WS-Fed for authentication purposes (i.e you just want to know who your users are), using sliding expiration is probably a good idea. But if you need to make some API calls on a remote server, your users may end up being authenticated for a long time, far after the expiration of their security token.

Resources