You’ll learn about alternatives to cookies using HTML 5 Web Storage and the standard Bearer authentication scheme for token-based authentication. You’ll enable cross-origin resource sharing (CORS) to allow cross-domain requests from the new site.
If you need to communticate with existing API from different domain, the same-origin policy (SOP) throws up several problems for cookie-based authentication:
- Attempting to send a login request from the new site is blocked because the JSON Content-Type header is disallowed by the SOP
- Even if you could send the request, the browser will ignore any Set-Cookie headers on a cross-origin response, so the session cookie will be discarded
- You also cannot read the anti-CSRF token, so cannot make requests from the new site even if the user is already logged in
Moving to an alternative token storage mechanism solves only the second issue, but if you want to allow cross-origin requests to your API from browser clients, you’ll need to solve the others. The solution is the CORS standard, introduced in 2013 to allow the SOP to be relaxed for some cross-origin requests
You can learn more about CORS headers from Mozilla’s excellent article
at https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. The Access-Control-Allow-Origin
and
Access-Control-Allow-Credentials
headers can be sent in the response to the preflight request and
in the response to the actual request, whereas the other headers are sent only in response to
the preflight request
NOTE Because cookies are considered a credential by CORS, you need to return an Access-Control-Allow-Credentials: true header from preflight requests; otherwise, the browser will not send the session cookie.
Start a server
mvn clean compile exec:java
Now start a second copy of the Natter UI by running the following command:
mvn clean compile exec:java -Dexec.args=9999 -pl "chapter_04_modern_token_auth"
You’d like to mark your cookies as SameSite as a defense in depth against CSRF attacks, but SameSite cookies are incompatible with CORS. Apple’s Safari browser is also aggressively blocking cookies on some cross-site requests for privacy reasons, and some users are doing this manually through browser settings and extension
Although the SQL database storage used in this chapter is adequate for demonstration purposes and low-traffic APIs, a relational database may not be a perfect choice for all deployments. Authentication tokens are validated on every request, so the cost of a database transaction for every lookup can soon add up. On the other hand, tokens are usually extremely simple in structure, so they don’t need a complicated database schema or sophisticated integrity constraints. At the same time, token state rarely changes after a token has been issued, and a fresh token should be generated whenever any security-sensitive attributes change to avoid session fixation attacks. This means that many uses of tokens are also largely unaffected by consistency worries.
For these reasons, many production implementations of token storage opt for non-relational database backends, such as the Redis in-memory key-value store (https://redis.io), or a NoSQL JSON store that emphasizes speed and availability.
Whichever database backend you choose, you should ensure that it respects consistency in one crucial aspect: token deletion. If a token is deleted due to a suspected security breach, it should not come back to life later due to a glitch in the database. The Jepsen project (https://jepsen.io/analyses) provides detailed analysis and testing of the consistency properties of many databases.
The first thing you need to do when issuing a new token is to generate a fresh token ID.
You shouldn’t use a normal database sequence for this, because token IDs must be unguessable for
an attacker. Otherwise an attacker can simply wait for another user to login and then guess the ID of
their token to hijack their session. IDs generated by database sequences tend to be extremely predictable,
often just a simple incrementing integer value. To be secure, a token ID should be generated with a
high degree of entropy from a cryptographically-secure random number generator (RNG). In Java, this means
the random data should come from a SecureRandom
object.
DEFINITION In information security, entropy is a measure of the randomness or unpredictability of data. It is often used to describe the strength of encryption keys or passwords. The more entropy a variable has, the more difficult it is to guess what value it has. For long-lived values that should be un-guessable by an adversary with access to large amounts of computing power, an entropy of 128 bits is a secure minimum. If your API issues a very large number of tokens with long expiry times, then you should consider a higher entropy of 160 bits or more. For short-lived tokens and an API with rate-limiting on token validation requests, you could reduce the entropy to reduce the token size, but this is rarely worth it.
- create a test user
curl -i --cacert "$(mkcert -CAROOT)/rootCA.pem" -H 'Content-Type: application/json' -d '{"username":"test","password":"password"}' https://localhost:4567/users
- create a token
curl -i --cacert "$(mkcert -CAROOT)/rootCA.pem" -u test:password -H 'Content-Type: application/json' -X POST https://localhost:4567/sessions
Note the lack of a Set-Cookie
header in the response. There is just the new token in the JSON body.
Passing the token in a X-CSRF-Token
header is less than ideal for tokens that have nothing to do
with CSRF. You could just rename the header, and that would be perfectly acceptable.
However, a standard way to pass non-cookie-based tokens to an API exists in the form of the Bearer
token scheme for HTTP authentication defined by RFC 6750.
While originally designed for OAuth2 usage, the scheme has been widely adopted as a general mechanism
for API token-based authentication.
NOTE Any client that has a valid token is authorized to use that token and does not need to supply any further proof of authentication. A bearer token can be given to a third party to grant them access without revealing user credentials but can also be used easily by attackers if stolen.
To send a token to an API using the Bearer scheme, you simply include it in an Authorization header:
Authorization: Bearer QDAmQ9TStkDCpVK5A9kFowtYn2k
The standard also describes how to issue a WWW-Authenticate
challenge header for bearer tokens, which allows our API
to become compliant with the HTTP specifications. The challenge can include a realm parameter if the API requires
different tokens for different endpoints. For example, you might return realm="users"
from one endpoint
and realm="admins"
from another, to indicate to the client that they should obtain a token from a
different login endpoint for administrators compared to regular users
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="users", error="invalid_token", error_description="Token has expired"
Until the release of HTML 5, there were very few alternatives to cookies for storing tokens in a web browser client. Now there are two widely-supported alternatives:
- The Web Storage API that includes the
localStorage
andsessionStorage
objects for storing simple key-value pairs. - The IndexedDB API that allows storing larger amounts of data in a more sophisticated JSON NoSQL database.
- The
sessionStorage
object can be used to store data until the browser window or tab is closed. - The
localStorage
object stores data until it is explicitly deleted, saving the data even over browser restarts.
Although similar to session cookies, sessionStorage is not shared between browser tabs or windows; each tab gets its own storage. Although this can be useful, if you use sessionStorage to store authentication tokens then the user will be forced to login again every time they open a new tab and logging out of one tab will not log them out of the others.
You should separate the database server from the API and ensure that the database is not directly accessible by external clients. Communication between the database and the API should be secured with TLS.
Authentication tokens are credentials that allow access to a user’s account, just like a password. In chapter 3, you learned to hash passwords to protect them in case the user database is ever compromised. You should do the same for authentication tokens, for the same reason. If an attacker ever compromises the token database, they can immediately use all the login tokens for any user that is currently logged in. Unlike user passwords, authentication tokens have high entropy, so you don’t need to use an expensive password hashing algorithm like Scrypt. Instead, you can use a fast, cryptographic hash function such as SHA-256
Although effective against token theft, simple hashing does not prevent an attacker with write access from inserting a fake token that gives them access to another user’s account. Most databases are also not designed to provide constant-time equality comparisons, so database lookups can be vulnerable to timing attacks. You can eliminate both issues by calculating a message authentication code (MAC), such as the standard hash-based MAC (HMAC). HMAC works like a normal cryptographic hash function, but incorporates a secret key known only to the API server.
DEFINITION A message authentication code (MAC) is an algorithm for computing a short fixed-length authentication tag from a message and a secret key. A user with the same secret key will be able to compute the same tag from the same message, but any change in the message will result in a completely different tag.
The key used for HMAC-SHA256 is just a 32-byte random value, so you could generate one using a
SecureRandom
just like you currently do for database token IDs. But many APIs will be implemented
using more than one server to handle load from large numbers of clients, and requests from the same
client may be routed to any server, so they all need to use the same key. Otherwise, a token generated
on one server will be rejected as invalid by a different server with a different key. Even if you have
only a single server, if you ever restart it, then it will reject tokens issued before it restarted
unless the key is the same. To get around these problems, you can store the key in an external
keystore
that can be loaded by each server.
DEFINITION A keystore is an encrypted file that contains cryptographic keys and TLS certificates used by your API. A keystore is usually protected by a password.
Java supports loading keys from keystores using the java.security.KeyStore
class, and you can create a keystore
using the keytool command shipped with the JDK. Java provides several keystore formats, but you should use
the [PKCS #12](https:// tools.ietf.org/html/rfc7292) format because that is the most secure option supported by keytool
keytool -genseckey -keyalg HmacSHA256 -keysize 256 \
-alias hmac-key -keystore keystore.p12 \
-storetype PKCS12 \
-storepass changeit