欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

PHP Authorization with JWT (JSON Web Tokens)

If you like computer security topics, you will know that one of the most discussed and controversial topics is user authentication. Within its context, you will find a broad range of study areas, from new mechanisms to usability. It is, thus, to my surprise that JSON Web Tokens is a topic not often talked about, and I think it deserves to be in the spotlight today. We will see how easy it is to integrate it in an API authentication mechanism.

Key icon

Versus Sessions

There was a time when the only way to authenticate yourself into an application was by giving out credentials. Later came service APIs and sending out credentials in plain text was unacceptable. The idea of API tokens came up and nowadays, they are common practice.

Some of the disadvantages of giving out credentials to an application and maintaining a user’s state in relation to the application with session cookies are:

  • Data is stored in plain text on the server 
    Even though the data is usually not stored in a public folder, anyone with access can read the contents of the session files.

  • Filesystem read/write requests 
    Every time a session starts or its data is modified, the server needs to update the session file. The same goes for every time the application sends a session cookie. You will end up with a slow server if you have a considerable amount of users, unless you use alternative session stores.

  • Distributed/clustered applications 
    Since the session files are stored in the file system by default, it is hard to have a distributed or clustered infrastructure for high availability applications that require the use of load balancers, clustered servers, etc… Other storage media and special configurations have to be made.

When dealing with service APIs that have restricted service calls, you will need to add your key to every request made (either in the request header, such as Authorization, or in the URL query string). API keys commonly rely on a centralized mechanism to control them. So if you want to mark an API key as invalid, it has to be revoked on the application side.

JWT

Since October 2010, there have been several proposals to use JSON based tokens. JWT or JSON Web Token was proposed on December 2010, having the following characteristics:

  • Intended for space constrained environments, such as HTTP Authorization headers or query string parameters.

  • Data to be transmitted in Javascript Object Notation format (JSON)

  • The data has to be the payload of a JSON Web Signature (JWS)

  • Represented using Base64 URL encoding

The JSON Web Signature is a cryptographic mechanism designed to secure data with a digital signature unique to the contents of the token in such a way that we are able to determine whether the data of the token has been tampered with or not.

The use of JWTs has many advantages over a single API key:

  • API keys are just random strings, while JWTs contain information and metadata that can describe user identity, authorization data and the validity of the token within a time frame or domain.

  • JWTs do not require a centralized issuing or revoking authority.

  • OAUTH2 compatible.

  • JWT data can be inspected.

  • JWTs have expiration controls.

On May 19th 2015, JWT became a published IETF RFC 7519.

What does it look like?

A JWT would look like the following:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E

It would appear that the string is just random characters concatenated together, and not very different from an API key. However, if you look closely, there are actually 3 strings, separated by a dot character.

The first and second strings are Base64 URL encoded JSON strings, so if we decode those, we will have the following results:

{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "iat": 1416929109,
  "jti": "aa7f8d0a95c",
  "scopes": [
    "repo",
    "public_repo"
  ]
}

The first string is the JWS header, which states which cryptographic algorithm was used to generate the signature and the payload type. The second string is the payload, and passes along some standard fields, any data you wish to send within the token. The third string is the cryptographic signature, and will decode to binary data.

What is interesting about the signature is that the cryptographic algorithm requires a secret key, a string that only the issuer application has to know and should never be disclosed in any way. This way when the application receives a token, it can verify the signature against the contents of the token using said secret key. If the signature verification fails, we can know for sure that the data within the token has been tampered with and should be discarded.

You can take a look at jwt.io where you can play around with encoding and decoding JWTs.

Let’s Play

So how do we apply this to a PHP app? Let’s say we have a login mechanism that currently uses session cookies to store information about a user’s login state within the application. Please note that JWT was not designed to substitute session cookies. However, for this example, we will have a couple of services: one that generates a JWT based on the provided username and password, and another that will fetch a protected resource provided we supply a valid JWT.

Login page

Once we are signed in, we will be able to retrieve a protected resource from the application.

To begin, we install php-jwt with composer require firebase/php-jwt. In the sample application developed for this tutorial, I’m also using zend-config and zend-http, so if you’d like to follow along, feel free to install those as well:

composer require firebase/php-jwt:dev-master
composer require zendframework/zend-config:~2.3
composer require zendframework/zend-http:~2.3

There is another PHP library, jose from namshi if you would like to play with it later on.

Now, let’s assume that the login form submits the data to our JWT issuer service via AJAX, where the credentials are validated against a database, and after determining that the credentials are valid, we have to build our token. Let’s build it as an array first:

<?php
require_once('vendor/autoload.php');

/*
 * Application setup, database connection, data sanitization and user  
 * validation routines are here.
 */
$config = Factory::fromFile('config/config.php', true); // Create a Zend Config Object

if ($credentialsAreValid) {

    $tokenId    = base64_encode(mcrypt_create_iv(32));
    $issuedAt   = time();
    $notBefore  = $issuedAt + 10;             //Adding 10 seconds
    $expire     = $notBefore + 60;            // Adding 60 seconds
    $serverName = $config->get('serverName'); // Retrieve the server name from config file
    
    /*
     * Create the token as an array
     */
    $data = [
        'iat'  => $issuedAt,         // Issued at: time when the token was generated
        'jti'  => $tokenId,          // Json Token Id: an unique identifier for the token
        'iss'  => $serverName,       // Issuer
        'nbf'  => $notBefore,        // Not before
        'exp'  => $expire,           // Expire
        'data' => [                  // Data related to the signer user
            'userId'   => $rs['id'], // userid from the users table
            'userName' => $username, // User name
        ]
    ];

     /*
      * More code here...
      */
}

Please notice that you can define the data structure however you want, there are however some reserved claims, such as the ones used above:

  • iat – timestamp of token issuing.

  • jti – A unique string, could be used to validate a token, but goes against not having a centralized issuer authority.

  • iss – A string containing the name or identifier of the issuer application. Can be a domain name and can be used to discard tokens from other applications.

  • nbf – Timestamp of when the token should start being considered valid. Should be equal to or greater than iat. In this case, the token will begin to be valid 10 seconds 
    after being issued.

  • exp – Timestamp of when the token should cease to be valid. Should be greater than iat and nbf. In this case, the token will expire 60 seconds after being issued.

Those claims are not required, but will help you determine the validity of a token (more on this later). Our application’s payload comes inside the data claim, where we are storing the userId and userName values. Since a JWT can be inspected client side, please remember not to include any sensitive information in it.

Transforming this array into a Jis super easy:

<?php

/*
 * Code here...
 */

    /*
     * Extract the key, which is coming from the config file. 
     * 
     * Best suggestion is the key to be a binary string and 
     * store it in encoded in a config file. 
     *
     * Can be generated with base64_encode(openssl_random_pseudo_bytes(64));
     *
     * keep it secure! You'll need the exact key to verify the 
     * token later.
     */
    $secretKey = base64_decode($config->get('jwtKey'));
    
    /*
     * Encode the array to a JWT string.
     * Second parameter is the key to encode the token.
     * 
     * The output string can be validated at http://jwt.io/
     */
    $jwt = JWT::encode(
        $data,      //Data to be encoded in the JWT
        $secretKey, // The signing key
        'HS512'     // Algorithm used to sign the token, see https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3
        );
        
    $unencodedArray = ['jwt' => $jwt];
    echo json_encode($unencodedArray);

JWT::encode() will take care of everything (transforming the array to JSON, producing the headers, signing the payload and encoding the final string). You will want to make your secret key a long, binary string, encode it in a config file and never disclose it. Having it directly in your code is a bad idea.

Now that the client has the token, you can store it using JS or whichever mechanism you like. Here is an example using jQuery:

$(function(){
    var store = store || {};
    
    /*
     * Sets the jwt to the store object
     */
    store.setJWT = function(data){
        this.JWT = data;
    }
    
    /*
     * Submit the login form via ajax
     */
	$("#frmLogin").submit(function(e){
	        e.preventDefault();
	        $.post('auth/token', $("#frmLogin").serialize(), function(data){
	            store.setJWT(data.JWT);
	        }).fail(function(){
	            alert('error');
	        });
	    });
});

Now let’s retrieve a resource that is protected by our JWT mechanism.

Sample UI

When clicking on the “Get resource >>” button, if everything is alright, you should see an image in the grey area. Let’s use an ajax call to send the request to the resource service:

$("#btnGetResource").click(function(e){
        e.preventDefault();
        $.ajax({
            url: 'resource/image',
            beforeSend: function(request){
                request.setRequestHeader('Authorization', 'Bearer ' + store.JWT);
            },
            type: 'GET',
            success: function(data) {
                // Decode and show the returned data nicely.
            },
            error: function() {
                alert('error');
            }
        });
    });

Please notice the beforeSend option. We are telling jQuery that before every request is made through this call, we need to set the Authorization header with the contents of the JWT in the format of Bearer [JWT]. So when we click the button, the following request is made:

GET /resource.php HTTP/1.1
Host: yourhost.com
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttpRequest
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0

Now we can see what the protected resource is:

Kitteh

This is how we validate the token in the resource service.

<?php
chdir(dirname(__DIR__));

require_once('vendor/autoload.php');

use Zend\Config\Config;
use Zend\Config\Factory;
use Zend\Http\PhpEnvironment\Request;

/*
 * Get all headers from the HTTP request
 */
$request = new Request();

if ($request->isGet()) {
    $authHeader = $request->getHeader('authorization');

    /*
     * Look for the 'authorization' header
     */
    if ($authHeader) {
        /*
         * Extract the jwt from the Bearer
         */
        list($jwt) = sscanf( $authHeader->toString(), 'Authorization: Bearer %s');

        if ($jwt) {
            try {
                $config = Factory::fromFile('config/config.php', true);

                /*
                 * decode the jwt using the key from config
                 */
                $secretKey = base64_decode($config->get('jwtKey'));
                
                $token = JWT::decode($jwt, $secretKey, array('HS512'));

                $asset = base64_encode(file_get_contents('http://lorempixel.com/200/300/cats/'));

                /*
                 * return protected asset
                 */
                header('Content-type: application/json');
                echo json_encode([
                    'img'    => $asset
                ]);

            } catch (Exception $e) {
                /*
                 * the token was not able to be decoded.
                 * this is likely because the signature was not able to be verified (tampered token)
                 */
                header('HTTP/1.0 401 Unauthorized');
            }
        } else {
            /*
             * No token was able to be extracted from the authorization header
             */
            header('HTTP/1.0 400 Bad Request');
        }
    } else {
        /*
         * The request lacks the authorization token
         */
        header('HTTP/1.0 400 Bad Request');
        echo 'Token not found in request';
    }
} else {
    header('HTTP/1.0 405 Method Not Allowed');
}

I’m using Zend\Http\PhpEnvironment\Request to make things a little easier to deal with extracting HTTP request types and headers:

$request = new Request();
if ($request->isGet()) { //Will only process HTTP GET requests.
	$authHeader = $request->getHeader('authorization');
	// ...

Now let’s find out if the authorization header has a JWT string in it:

/*
 * Look for the 'authorization' header
 */
if ($authHeader) {
    /*
     * Extract the JWT from the Bearer
     */
    list($jwt) = sscanf( $authHeader->toString(), 'Authorization: Bearer %s');
    // MORE CODE
}

This way the variable $jwt will have the contents of a potential JWT.

One alternative you might choose if you do not want to deal with HTTP Authorization headers, is to include the token in the request as a URL parameter:

GET /resource.php?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0 HTTP/1.1
Host: yourhost.com
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttpRequest

Let’s try to decode the JWT now. Remember the secret key we used earlier to generate the token? It is a vital part of the decoding process here:

$secretKey = base64_decode($config->get('jwtKey'));

/*
 * decode the JWT using the key from config
 */
$token = JWT::decode($jwt, $secretKey, array('HS512'));

If the process to decode the JWT fails, it could be that:

  1. The number of segments provided did not match the standard 3 as described earlier.

  2. The header or the payload is not a valid JSON string

  3. The signature is invalid, which means the data was tampered with!

  4. The nbf claim is set in the JWT with a timestamp, when the current timestamp is less than that.

  5. The iat claim is set in the JWT with a timestamp, when the current timestamp is less than that.

  6. The exp claim is set in the JWT with a timestamp, when the current timestamp is more than that.

As you can see, JWT has a nice set of controls that will mark it as invalid, without the need to manually revoke it or check it against a list of valid tokens.

In case you were wondering about the JWT signature and tampered data, this is possible thanks to cryptographic Message authentication codes. In a nutshell, arbitrary data input along with a key will produce a unique ‘fingerprint’ of the data. This fingerprint alone cannot be reversed back to the data input and the slightest change to either the data input or the key will produce a totally different fingerprint.

At this point we can be sure that the JWT is valid. Additionally, you could check if the user in the token is still valid, if the issuer of the token (from the iss claim) is you, or if your token has embedded permission flags, then check those against the action the user is requesting to perform.

Finally, we request an image from lorempixel.com, base64 encode it and return it in a json response string:

$asset = base64_encode(file_get_contents('http://lorempixel.com/200/300/cats/'));

/*
 * return protected asset
 */
header('Content-type: application/json');
echo json_encode([
    'img'    => $asset
]);

If you want to play with a sample application, you can check out my project’s repo for this article, follow the instructions of the README, and take a closer look at the code.

sample app

Conclusion

From here on, you can try to implement JWTs in your next API, maybe trying some other signing algorithms that use asymmetric keys like RS256 or integrate it in an existing OAUTH2 authentication server to be the API key. All your constructive feedback is welcome, as well as any questions or comments.

SPONSORS


普通分类: