We are keen on security - recently we have published the Node.js Security Checklist. As a sequel, let's dive deep into the world of cookies, tokens and other web authentication methods. If you’d like to learn more about the basic authentication strategies with Passport.js, check out our beginner guide here.
We are going to start with the most basic one, the HTTP Basic authentication, continue with cookies and tokens, and finish up with signatures and one-time passwords.
HTTP Basic authentication
HTTP Basic authentication is a method for the client to provide a username and a password when making a request.
This is the simplest possible way to enforce access control as it doesn't require cookies, sessions or anything else. To use this, the client has to send the
Authorization header along with every request it makes. The username and password are not encrypted, but constructed this way:
- username and password are concatenated into a single string:
- this string is encoded with Base64
Basickeyword is put before this encoded value
Example for a user named
john with password
curl --header "Authorization: Basic am9objpzZWNyZXQ=" my-website.com
The same can be observed in Chrome as well:
Implementing it is pretty easy in Node.js as well - the following snippet shows how you can do an Express middleware to do so.
Of course, you can do it on a higher level, like in nginx.
Looks simple, right? So what are the drawbacks of using HTTP Basic authentication?
- the username and password are sent with every request, potentially exposing them - even if sent via a secure connection
- connected to SSL/TLS, if a website uses weak encryption, or an attacker can break it, the usernames and passwords will be exposed immediately
- there is no way to log out the user using Basic auth
- expiration of credentials is not trivial - you have to ask the user to change password to do so
When a server receives an HTTP request in the response, it can send a
Set-Cookie header. The browser puts it into a cookie jar, and the cookie will be sent along with every request made to the same origin in the
Cookie HTTP header.
Always use HttpOnly cookies
To mitigate the possibility of XSS attacks always use the
HttpOnly flag when setting cookies. This way they won't show up in
Always use signed cookies
With signed cookies, a server can tell if a cookie was modified by the client.
This can be observed in Chrome as well - first let's take a look at how a server set cookies:
Later on all the requests use the cookies set for the given domain:
- Need to make extra effort to mitigate CSRF attacks
- Incompatibility with REST - as it introduces a state into a stateless protocol
Nowadays JWT (JSON Web Token) is everywhere - still it is worth taking a look on potential security issues.
First let's see what JWT is!
JWT consists of three parts:
- Header, containing the type of the token and the hashing algorithm
- Payload, containing the claims
- Signature, which can be calculated as follows if you chose HMAC SHA256:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Adding JWT to Koa applications is only a couple of lines of code:
Example usage - (to check out the validity/content of the token, you can use jwt.io):
curl --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" my-website.com
As the previous ones, the tokens can be observed in Chrome as well:
If you are writing APIs for native mobile applications or SPAs, JWT can be a good fit. One thing to keep in mind: to use JWT in the browser you have to store it in either LocalStorage or SessionStorage, which can lead to XSS attacks.
- Need to make extra effort to mitigate XSS attacks
Either using cookies or tokens, if the transport layer for whatever reason gets exposed your credentials are easy to access - and with a token or cookie the attacker can act like the real user.
A possible way to solve this - at least when we are talking about APIs and not the browser is to sign each request. How does that work?
When a consumer of an API makes a request it has to sign it, meaning it has to create a hash from the entire request using a private key. For that hash calculation you may use:
- HTTP method
- Path of the request
- HTTP headers
- Checksum of the HTTP payload
- and a private key to create the hash
To make it work, both the consumer of the API and the provider have to have the same private key. Once you have the signature, you have to add it to the request, either in query strings or HTTP headers. Also, a date should be added as well, so you can define an expiration date.
AWS Request Signing Flow - source
Why go through all these steps? Because even if the transport layer gets compromised, an attacker can only read your traffic, won't be able to act as a user, as the attacker will not be able to sign requests - as the private key is not in his/her possession. Most AWS services are using this kind of authentication.
node-http-signature deals with HTTP Request Signing and worth checking out.
- cannot use in the browser / client, only between APIs
One-Time passwords algorithms generate a one-time password with a shared secret and either the current time or a counter:
- Time-based One-time Password Algorithm, based on the current time,
- HMAC-based One-time Password Algorithm, based on a counter.
These methods are used in applications that leverage two-factor authentication: a user enters the username and password then both the server and the client generates a one-time password.
In Node.js, implementing this using notp is relatively easy.
- with the shared-secret (if stolen) user tokens can be emulated
- because clients can be stolen / go wrong every real-time application have methods to bypass this, like an email reset that adds additional attack vectors to the application
Which web authentication method to pick when?
If you have to support a web application only, either cookies or tokens are fine - for cookies think about XSRF, for JWT take care of XSS.
If you have to support both a web application and a mobile client, go with an API that supports token-based authentication.
If you are building APIs that communicate with each other, go with request signing.
You have additional thoughts or insights on the topic? Share it in the comments.