Self-Encoded Access Tokens

8.5

Self-encoded tokens provide a way to avoid storing tokens in a database by encoding all of the necessary information in the token string itself. The main benefit of this is that API servers are able to verify access tokens without doing a database lookup on every API request, making the API much more easily scalable.

The benefit of OAuth 2.0 Bearer Tokens is that applications don’t need to be aware of how you’ve decided to implement access tokens in your service. This means it’s possible to change your implementation later without affecting clients.

If you already have a distributed database system that is horizontally scalable, then you may not gain any benefits by using self-encoded tokens. In fact, using self-encoded tokens if you’ve already solved the distributed database problem will only introduce new issues, as invalidating self-encoded tokens becomes an additional hurdle.

There are many ways to self-encode tokens. The actual method you choose is only important to your implementation, since the token information is not exposed to external developers.

One way to create self-encoded tokens is to create a JSON-serialized representation of all the data you want to include in the token, and sign the resulting string with a key known only to your server.

A common technique for this is using the JSON Web Signature (JWS) standard to handle encoding, decoding and verification of tokens. The JSON Web Token (JWT) specification defines some terms you can use in the JWS, as well as defines some timestamp terms to determine whether a token is valid. We’ll use a JWT library in this example, since it provides built-in handling of expiration.

Encoding

The code below is written in PHP and uses the Firebase PHP-JWT library to encode and verify tokens. You’ll need to include that library in order to run the sample code

<?php
use \Firebase\JWT\JWT;

# Define the secret key used to create and verify the signature
$jwt_key = 'secret';

# Set the user ID of the user this token is for
$user_id = 1000;

# Set the client ID of the app that is generating this token
$client_id = 'https://example-app.com';

# Provide the list of scopes this token is valid for
$scope = 'read write';

$token_data = array(

  # Subject (The user ID)
  'sub' => $user_id,

  # Issuer (the token endpoint)
  'iss' => 'https://' . $_SERVER['PHP_SELF'],

  # Audience (intended for use by the client that generated the token)
  'aud' => $client_id,

  # Issued At
  'iat' => time(),

  # Expires At
  'exp' => time()+7200, // Valid for 2 hours

  # The list of OAuth scopes this token includes
  'scope' => $scope
);
$token_string = JWT::encode($token_data, $jwt_key);

This will result in a string such as:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOjEwMDAsImlzcyI6Imh0dHBzOi8
vYXV0aG9yaXphdGlvbi1zZXJ2ZXIuY29tIiw
iYXVkIjoiaHR0cHM6Ly9leGFtcGxlLWFwcC5
jb20iLCJpYXQiOjE0NzAwMDI3MDMsImV4cCI
6MTQ3MDAwOTkwMywic2NvcGUiOiJyZWFkIHd
yaXRlIn0.zhVmPMfS3_Ty4qUl5ZMh4TipXsU
CSH0mHzb4P_Ijhxs

This token is made up of three components, separated by periods. The first part describes the signature method used. The second part contains the token data. The third part is the signature.

For example, this token’s first component is this JSON object:

{
   "typ":"JWT",
   "alg":"HS256”
 }

The second component contains the actual data the API endpoint needs in order to process the request, such as user identification and scope access.

{
  "sub": 1000,
  "iss": "https://authorization-server.com",
  "aud": "https://example-app.com",
  "iat": 1470002703,
  "exp": 1470009903,
  "scope": "read write"
}

Base64-encoding the first two components results in these following two strings:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
eyJzdWIiOjEwMDAsImlzcyI6Imh0dHBzOi8vYXV0aG9yaXphdGlvbi1z
ZXJ2ZXIuY29tIiwiYXVkIjoiaHR0cHM6Ly9leGFtcGxlLWFwcC5jb20i
LCJpYXQiOjE0NzAwMDI3MDMsImV4cCI6MTQ3MDAwOTkwMywic2NvcGUi
OiJyZWFkIHdyaXRlIn0

We then calculate a hash of the two strings along with a secret, resulting in another string:

zhVmPMfS3_Ty4qUl5ZMh4TipXsUCSH0mHzb4P_Ijhxs

Finally, concatenate all three strings together separated by periods.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOjEwMDAsImlzcyI6Imh0dHBzOi8
vYXV0aG9yaXphdGlvbi1zZXJ2ZXIuY29tIiw
iYXVkIjoiaHR0cHM6Ly9leGFtcGxlLWFwcC5
jb20iLCJpYXQiOjE0NzAwMDI3MDMsImV4cCI
6MTQ3MDAwOTkwMywic2NvcGUiOiJyZWFkIHd
yaXRlIn0.zhVmPMfS3_Ty4qUl5ZMh4TipXsU
CSH0mHzb4P_Ijhxs

Decoding

Verifying the access token can be done by using the same JWT library. The library will decode and verify the signature at the same time, and throws an exception if the signature was invalid, or if the expiration date of the token has already passed.

Note: Anyone can read the token information by base64-decoding the middle section of the token string. For this reason, it’s important that you do not store private information or information you do not want a user or developer to see in the token. If you want to hide the token information, you can use the JSON Web Encryption spec to encrypt the data in the token.
try {
  # Note: You must provide the list of supported algorithms in order to prevent 
  # an attacker from bypassing the signature verification. See:
  # https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
  $token = JWT::decode($token_string, $jwt_key, ['HS256']);
  $error = false;
} catch(\Firebase\JWT\ExpiredException $e) {
  $token = false;
  $error = 'expired';
  $error_description = 'The token has expired';
} catch(\Firebase\JWT\SignatureInvalidException $e) {
  $token = false;
  $error = 'invalid';
  $error_description = 'The token provided was malformed';
} catch(Exception $e) {
  $token = false;
  $error = 'unauthorized';
  $error_description = $e->getMessage();
}

if($error) {
  header('HTTP/1.1 401 Unauthorized');
  echo json_encode(array(
    'error'=>$error, 
    'error_description'=>$error_description
  ));
  die();
} else {
  // Now $token has all the data that we encoded in it originally
  print_r($token);
}

At this point, the service has all the information it needs such as the user ID, scope, etc, available to it, and didn’t have to do a database lookup. Next it can check to make sure the access token hasn’t expired, can verify the scope is sufficient to perform the requested operation, and can then process the request.

Invalidating

Because the token can be verified without doing a database lookup, there is no way to invalidate a token until it expires. You’ll need to take additional steps to invalidate tokens that are self-encoded. See Refreshing Access Tokens for more information.