How to authenticate against Web APIs
About JSON Web Tokens (JWT), Cookies, XSRF, XSS and how to get it all right, more or less.
In the good old times we just had PHP, would set a Session-Cookie and it all kind of worked out. In my first big project as a teenager, a small special interest community/bulletin board written by myself in PHP, security was not really an issue. TLS was still called SSL and was only used by banking sites. We hashed passwords with MD5 or not at all and it was okay-ish.
About 15 years later, I make Web APIs for a living, and boy did times change. Even for graduated computer scientists it is somehow confusing how to get Authentication “right”, in a way that you can sleep well at night without the constant fear that you did a major mistake and your user’s data is basically an open book for a potential attacker. I’ll try to summarize the current state of Web Authentication and how we’re doing it.
Dieser Artikel erschien zuerst auf Medium, als es cool war dort zu posten. Habe ich nur einmal gemacht, und wer weiß was dort mit den Inhalten passiert. Daher durfte er jetzt hierher umziehen, wo er nicht verloren gehen kann.
Many articles compare JWT, or token-based authentication, to Cookies. This is confusing because JWT is a token format and cookies are a storing mechanism. So let’s get back to square one at first:
- We have some sort of data that is “owned” by a user in some way, and we have to make sure only the user can access/modify it.
- We need to make sure the user is who he tells us he is (Authentication)
- We need to make sure the user is allowed to do the desired action (Authorization)
Authentication is usually done by supplying something only the user can know or have, commonly using the username/password pattern. This is not the best solution, a public/private key authentication would still be better and browsers support it, but it is not very practical to manage for more than a handful of users (but for internal systems, administration interfaces and so on, it should be the way to go!).
So we have to check a user’s password and give him a key he can then use for some time (because we don’t want to check the password for each single request). In this process we need to make sure
- we never store the password on our end
- the password is transmitted securely
- the token is never stored on our end
- the token is transmitted securely
- the token is stored on the user’s end securely
- the token is valid when used
To check the “securely”, we need to be aware of attack vectors first.
Attack vectors
- Man-In-The-Middle (MITM): someone intercepts the connection and can read or alter the stuff we transmit
- Cross-Site-Scripting (XSS): someone places malicious code on our website which is then trusted by the browser (because it seems to be from us)
- Cross-Site-Request-Forgery (XSRF): someone tricks the user to trigger an action in our system from another web site, while the browser thinks the request is valid
Of course, there are even more attack vectors, e.g. we should not store passwords or tokens in our database in case we lose it.
But what are the different possibilities now?
Transport-Layer-Security (TLS)
The most important thing to do is to always use HTTPS if non-public (user-) data is transmitted. HTTP without encryption is only a viable alternative on a pure read-only, public website (which is hardly anyone nowadays). Using TLS is the simplest and most effective solution against MITM attacks. The SSL Labs Test should give you an A or better.
Password Best Practice
This is really easy, and yet so often done wrong:
- Don’t make restrictions on your user’s passwords (allowed/disallowed/required characters, max length, …) — the only thing you may do is require a minimum length and display a strength indicator in the front-end, but without rejecting potentially weak passwords. And yes, it is possible that your bank is getting this one wrong. It is better the user has a not too cryptic password he can remember, than enforcing selected special characters and other rules which make your users writing the passwords down.
- Store only password hashes with a salt, and use a secure algorithm (MD5 is not secure). If you don’t know what a salt is, you should probably learn some more computer science before you continue your project.
- Never, ever generate a password and send it to your user via email or something like that. For password resets, send a link with a unique single-use token to the user’s email address.
- Never use “Security Questions”. If you want additional security, deploy Two-Factor-Authentication using RFC 6238.
Deploying Tokens
This brings me back to the often-heard “Cookies vs. JWT” which is comparing apples to oranges. Before we decide whether we use Cookies to store our tokens, we have to decide how our tokens look like. It may just be a “session ID” or something like that, e.g. a randomly generated string. But it is actually better to use JWTs: they can contain more information like expiration date, and most importantly: they are signed. So it is really easy to check if the token is valid and has not been tampered with. However, you should regard the following points:
- JWTs are signed, not encrypted. Everyone can read the content of your JWTs
- JWTs are like passwords, so you should never store them unhashed in your database
- Don’t lose or publish the private key you use to sign JWTs.
- JWTs only can tell you that you issued them (by checking the signature). Included permissions or validity dates may have changed on the back end. Because of that, don’t trust the content of the JWT to be still valid.
- Store validity and connected permissions (and not only check the signature for authentication). Otherwise you could never revoke a token or alter permissions of a user.
- JWTs are propagated as stateless. However, you don’t want that feature if you want to change validity of tokens or revoke them. The benefit of statelessness is none, you’ll still have to store parts of the JWT (never the whole!) on your server to get it right.
Storing Tokens in the Client
Now we are at the core of this article: Session-based authentication (“Cookie-based-authentication”) vs. Token-based authentication. As laid out before, the statelessness of JWTs is not really a good thing, so no point here for JWTs vs. Session-Cookies. While it is nice to use JWTs instead of simply random strings, it is not too much a difference. What is a difference, is how you authenticate with them and where clients store them.
- The cookie store in browsers is traditionally used. Cookies are tied to a hostname and sent from the browser automatically for requests to that server in a Cookie Header. Cookies can have a validity date after which they are deleted.
- Cookies can have the flag “secure” which tells browsers to only send them over TLS-secured connections, and the flag “httpOnly” to not make them available to XMLHttpRequests, i.e. JavaScript. It is browser-specific if these flags are respected.
- SessionStorage and LocalStorage are the newer alternatives, with the difference that SessionStorage is tied to the current tab, LocalStorage to the browser. Browsers never read from those stores automatically or send its contents with requests. But there are also no restrictions on reading from them or sending data only via TLS-secured connections. Data is also never deleted after some time from those stores, in contrast to the cookie store.
So where should we store our JWTs now?
In the cookie store, with the secure flag on, and the expiration date set correctly to the expiration date of the JWT. This provides some protection against XSS, at least — also, users are accustomed to “deleting cookies” being the same as “log me out everywhere”.
But: of course there is a “but”. Don’t accept cookie authentication in your API Server unless you really, really need to. Because cookies get sent automatically by the browser on requests to the matching host, XSRF attacks are possible (that thing when you come to a shady page which tricks you into posting something on your facebook wall). If you only use the cookie store as storing mechanism, but don’t read the cookie header on your server, you are safe against XSRF. However, you need to manually read the token out of the cookie using client-side JavaScript and put it in an Authorization header, see Bearer Authentication.
Of course, this only works if you go full JavaScript with a Single-Page-Application using Angular.js or React. If you want to use plain HTML with Authorization, i.e. authenticated GET-Requests or HTML forms, you’ll need to use Cookies (except you put the Bearer Token in the query string or as hidden property in the form).
To secure a request that gets authenticated via cookie-header on the server, you should use XSRF-Tokens: generate another JWT out of the same jid (or something else that is also in your auth-JWT and hard to guess) and put this JWT in your forms. Then check on the server that the ids from the auth-JWT and the XSRF-JWT are the same. This way it is guaranteed that the request ist coming from your site and not from an XSRF attack, because the attacker could not generate the correct XSRF token.
Of course, there is still no cure for XSS other than always sanitizing user input and never let users put <script> tags on your site — and be careful what third-party scripts you use.
Conclusion
- always use TLS (HTTPS)
- issue Access Tokens as JWTs
- store JWTs as secure Cookies in the browser
- if you only use JavaScript, don’t allow cookie-header authentication on the server
- if you must support cookie-header authentication, require an additional XSRF-Token and check that they match
- make sure no malicious JavaScript can be injected on your site