I gave a talk titled Handling Authentication Secrets in the Browser at Fluent 2017 in San Jose (you can see the slides above). As a complement to the talk, I thought it would be a good idea to write down the main concepts here on the blog as well, for those that weren't at my talk or those that were, but want to study the topic with more time than the 40 minutes I had for my presentation.
The Web Is Stateless
We tend to forget that the HTTP protocol is stateless. When working with protected resources, a client needs to send some form of identification along with a request, for example, a username and password that it obtained from the user. Once that request is complete, if the client needs to send a follow-up request, it will have to include identification again, since the server has no mechanism to "remember" the client from the previous request.
Related to this is the concept of logging in and out of web applications. At the HTTP protocol level, there is no such concept, this is implemented at a higher level through user sessions, and with the help of cookies or other HTTP headers.
Typical Forms of Authentication
The most common form of authentication is the username and password combination. A client application running in the browser asks the user for their credentials, and then forwards them to the server along with a request, typically in the
Authorization header. Web browsers have built-in support for the Basic and Digest authentication mechanisms from the HTTP standard, but the user experience is extremely bad, so in most cases applications do not rely on the browser for this and implement their own handling of the credentials.
Since it is not a good idea to constantly send user credentials to the server, a common pattern for web applications is that they require this type of authentication only for the first request a client sends. When the identity of the client is verified, the server establishes a user session for the client, and then exchanges the credentials for a reference to this user session.
For modern rich-client type applications, the server will typically return a token, that the client needs to send back to the server in the
Authorization header, or sometimes in a custom header that is specific to the application. For traditional thin-client web applications, it is very common for the server to return a reference to the user session in a cookie, so that the web browser automatically resends it back to the server with every request without the need to implement any application logic in the client.
Passwords, Tokens, Cookies... What's The Difference?
I've already mentioned the three most common forms of authentication in web applications, but how do they compare?
Passwords are in general created by humans, more specifically the users of the application. There are cases where servers assign randomly generated passwords to users when they open a new account, but those are considered highly insecure and are normally used to allow the user to login and immediately change the password to one of their own choosing. Tokens, on the other side, are always generated by the server.
It is common for tokens to have a scope associated with them, so for example, an administrator could have an option to request a regular user token while not performing administrative tasks, to avoid the risk of making changes by mistake. Passwords in general give full access to the user account, so losing a password could be much more devastating than losing a token.
Another area where passwords and tokens differ is in their validity. Passwords typically do not have expiration, or when they do, they last for a long time, typically several months. Tokens have shorter validity, and this reduces the risk of attacks if they are compromised.
Cookies are a general purpose storage mechanism that all web browsers support, so they don't really have much in common with passwords and tokens. However, I like to make a distinction between cookies in the general sense and authentication cookies, which are cookies that are specifically used for authentication. Cookies have the interesting property that the web browser sends them to the server automatically, without the application having to do anything, so they are many times used for authentication because of the convenience of having them handled by the browser.
I mentioned above that when a client sends a first request with "strong" authentication credentials such as username and password, the server exchanges those for a simpler form of identification which allows it to remember the client when it sends more requests in the future. This other identification mechanism that helps the server remember a client is the user session, which is a secure place where information about the authenticated client is stored.
A common place to store user sessions is a server controlled storage, as that is what makes it secure. This type of user sessions are typically stored in databases or disk files. In these cases, the server returns a session id to the client, which is an identifier that allows it to easily recall this user session whenever a new request from that client arrives.
Another style of user session is when the actual user session with all its data is sent to the client instead of just a reference to a server-side storage. This has the advantage that the server does not need to manage storage for the user sessions. To ensure that user sessions are not tampered with while in the client's possession, they are protected with a cryptographic signature that only the server knows how to generate. If the client attempts to modify the contents of the session, then the signature will not match the data anymore, and the server then rejects it. The JSON Web Tokens specification is a common solution for implementing signed user sessions.
Basic Protection of Secrets
With the basics out of the way, let's now review the measures that need to be taken to protect user credentials in a web application.
The absolute most important thing that needs to be done by every web application, is to use HTTPS. This ensures that the traffic between the server and the clients is always encrypted, making it impossible for intermediaries to eavesdrop on the communication and obtain any data included in HTTP requests or responses. An SSL certificate also allows clients to verify that they are talking to the actual server they are trying to reach and not an impostor. Since these days we have free SSL certificates, there is really no excuse for not implementing HTTPS. I have written an article about HTTPS that discusses the steps to secure your server (note that parts of this article are specific to securing a Python web application that uses the Flask framework, but the final, part where Let's Encrypt SSL certificates are discussed, applies to any type of web application).
So let's say you've implemented HTTPS for your application, so now your secrets are protected while in transit between client and server. The next thing you need to consider, is protecting your server to ensure attackers cannot steal secrets there. There are a number of relatively easy configuration changes that you should make:
- Use strong passwords for all SSH accounts, or better yet, only allow logins with SSH keys.
- Disable root logins
- Make sure that all cookies that store authentication have the
secureflag set, to prevent them from being sent over regular HTTP connections.
- Make sure only the services that need to be accessed publicly listen on public network ports. Typically, your database does not need to be accessible from outside your server or private network of servers, for example.
- Add a firewall that only exposes the necessary ports to the world, which may end up being just ports 80 and 443 for HTTP and HTTPS and port 22 for SSH.
- If your application crashes, make sure developer error information such as a stack trace is logged internally, and never returned in the HTTP error response.
- Keep your server up to date with security patches.
Web Browser Specific Attacks
Unfortunately, the measures from the previous section are not going to be enough to protect you against attacks. Web browsers are strange in that they hold session information for lots of different applications, and attackers are always looking for holes in the browser security model that allow them to access information from all these applications the browser knows about.
There are two very common vectors of attack specific to web browsers that occur because applications fail to protect secrets in the browser:
- Cross-Site Scripting, or XSS
- Cross-Site Request Forgery, or CSRF (or sometimes XSRF)
Besides having somewhat similarly cryptic names, these two are really completely different, and have different protection strategies.
Cross-Site Scripting Attacks
XSS is a type of code injection vulnerability that puts sites that have social features at risk. In particular, this type of attack can happen when users are allowed to write comments or posts that other users can see.
How to Protect Against XSS Attacks
Keep in mind that if your application does not allow users to contribute content that other users can see, then your risk of XSS attacks is much lower, attackers would have to find a way to make content they provide visible to other users, in an application that does not provide that level of sharing.
Now if your application does have social features, the most basic thing you should do to prevent XSS attacks is to routinely audit your code and make sure all input from users is sanitized, in particular with regards to comments or other social content users contribute. But because we are humans and we often tend to make mistakes, relying on audits alone is not enough. Luckily, there are a number of measures you can take to protect yourself against this nasty type of attack:
- Use authentication cookies, and make sure they have the
httpOnlyflag set. This is a fantastic protection against XSS attacks, since cookies that are
- Using as short an expiration as possible in your tokens is a good way to make sure that if a token is compromised, at least the window of time in which it can be exploited is not large. Obviously expiration times will have to be set according to the type of application you have.
Cross-Site Request Forgery Attacks
CSRF attacks are remote attacks, in the sense that you don't need to have your application infiltrated with the attacker's code to be vulnerable like in the XSS case.
How to Protect Against CSRF Attacks
The techniques to protect against CSRF are completely different, and even contradictory at times, to those we use for XSS. The basic level of protection for this attack vector is for your application to check the
The more involved measure that you can take against CSRF attacks is to implement a so called synchronizer token. The basic idea is that the server comes up with a random sequence of characters, which we will call a CSRF token, different from the token used for authentication purposes. The server keeps track of the CSRF token assigned to each client, and then expects the client to send it back with any follow-up requests. If the token does not match what the server has stored, then the request is discarded.
There are many possible implementations of this idea, but one that I like a lot involves the use of a cookie. The server writes the CSRF token to a cookie that does not have the
httpOnly flag, giving your client-side application full access to it. The client then reads the token from the cookie, and sends it back in all form posts and XHR requests, either in a custom header or a hidden form field, so basically the client will be sending the CSRF token twice, once in the cookie and once in another way. The server then needs to read the cookie and other value and ensure they are equal. An attacker's site will not be able to read the CSRF cookie, so it has no possibility of ever sending a request that looks authentic.
Another alternative that can be used instead of a synchronizer token, is to give the authentication token a second function as a CSRF token. If your application sends the token to the client in a
Authorization or custom header, this is enough protection against CSRF. If the token is stored in a cookie you are not protected though, because the browser will send the cookie from the attacker's site, as discussed above. An attacker's site will not be able to read your token... unless the token is compromised in a separate XSS attack! Recall that the best protection against XSS is to use authentication cookies, but for CSRF cookies can be a problem, so it is tricky to find the right security model.
Dealing With Compromised Credentials
After this excruciatingly long discussion of all the attack vectors and how they can be mitigated, I have really bad news to give you, and that is that your application is still vulnerable to attacks.
Unfortunately we live in an imperfect world. We developers make mistakes, that attackers are very good at exploiting. Users also make mistakes, or are careless in how they manage their security. So really there is only so much you can do to protect the credentials of a user, when that user uses the same password on many other sites, and some of those sites don't have the level of security you have on your own. If an attacker gets hold of a user's password on one site, it is really obvious that this password is going to be tested against other sites, so eventually, you will find that attackers can infiltrate your application, and there is really not much you can do about it.
So basically, my last piece of advice comes down to having a plan of action for when a vulnerability occurs. What does it mean to have a plan? I can list the following items for you to consider:
- If based on a user's complain you determine that a token is being used by an attacker, then you need to have a way to revoke that token immediately. I see many times this is not given a second thought, so then when a token is compromised there is no way to revoke it, or when there is, it involves someone hand editing database records, which is error prone, specially when you have to do it under pressure.
- If a user reports their account password was compromised, you need to have a way to reset the account so that the user can recover control. This means being able to revoke all active tokens for that user, ensuring the contact information has not been altered, and finally resetting the password on the account.
- If you think your own server might have been compromised, then you need to revoke your SSL certificate and get a new one, change all your account, database and other passwords, change your token signing key (which invalidates all tokens for all users), and reset all user accounts.
- You should have tests in place for the three items above, so that if bad luck strikes, you know you have a process in place you can rely on.
I hope you found this article useful in improving the security of your web application. Do you have any questions that are not covered in the article? Let me know below in the comments.