Skip to content

Latest commit

 

History

History

chapter_04_modern_token_auth

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Overview

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.

Allowing cross-domain requests with CORS

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

CORS headers

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.

Try it out

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"

Tokens without cookies

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

Storing token state in a database

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.

Generating Token ids

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.

Trying it out

  • 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.

The Bearer authentication scheme

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"

Storing tokens in Web Storage

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 and sessionStorage objects for storing simple key-value pairs.
  • The IndexedDB API that allows storing larger amounts of data in a more sophisticated JSON NoSQL database.

Bearer auth

  • 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.

Hardening database token storage

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.

Hashing database tokens

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

Authenticating tokens with HMAC

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.

HMAC

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.

Generating the key

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