BLOG | NGINX

Authentication and Content-Based Routing with JWTs and NGINX Plus

NGINX-Part-of-F5-horiz-black-type-RGB
Alan Murphy Thumbnail
Alan Murphy
Published March 01, 2018

NGINX Plus Release 10 introduced support for offloading authentication from web and API services with JSON Web Tokens (JWTs, pronounced “jots”). Since the release of R10, we’ve continued to increase functionality in each new release.

Starting in NGINX Plus R14, NGINX Plus supports JWTs that contain nested claims and array data. When used in an API gateway scenario, NGINX Plus can use JWTs to authenticate clients that are requesting connections to backend services and API destinations.

I’ve occasionally been asked to provide a basic configuration that uses NGINX Plus to authenticate JWTs, and then makes more advanced load‑balancing decisions based on JWT information. The most straightforward solution is simply to allow access to a service if authentication is successful, and block or redirect the connection if unsuccessful.

The walkthrough in this post is a soup-to-nuts proof of concept for JWT authentication and content‑based routing using NGINX Plus. To cover the broadest range of possibilities, and to reduce the need for prerequisite knowledge or experience with JWTs, I’ve created a “JWT 101” walkthrough, allowing you to deploy this solution (with examples and background) with no prior knowledge of JWTs.

If you already have JWT experience or existing JWTs in your environment, you can skip the first two sections, and adapt the provided NGINX Plus configuration snippets to suit your environment and start making advanced load‑balancing decisions based on your JWT claims data.

Prerequisites

This document assumes a fresh install of NGINX Plus, with default configuration files in the following locations:

  • /etc/nginx/nginx.conf
  • /etc/nginx/conf.d/default.conf

For more information about installing and getting started with NGINX Plus, see the NGINX Plus Admin Guide.

All CLI commands assume root privilege, so non‑root users must have sudo permissions in their environment.

For additional background information, and for other use cases for JWTs with NGINX Plus, see the following links:

Creating a JWT and Associated Signing Key

The instructions below walk you through creating a JWT from scratch with payload data specific to our example, as an illustration of how to configure NGINX Plus for basic processing of JWT claims. If you use an existing JWT instead of the sample one, you need to make sure that your “secrets” file contains the Base64URL‑encoded string that matches the signing key you use to create the JWT. You probably also need to modify the claims in the JWT payload.

Note: No matter how you generate your JWT, you must use Base64URL encoding, which correctly handles padding (done with the = character) as well as other non‑HTTP compliant characters typically used in Base64 encoding. Many tools handle this automatically, but manual CLI‑based Base64 encoders and some JWT‑creation tools do not. For more info on Base64URL encoding, see base64url encoding by Brock Allen and RFC 4648.

For this example, we’re using the GUI at jwt.io (which correctly does Base64URL encoding) to create a symmetric HS256 JWT. The following screenshot shows how the GUI looks after you enter the values specified in the instructions below and the signature is verified.

Working in the GUI at jwt.io, generate an HS256 JWT by verifying or inserting the indicated values in the fields of the Decoded column on the right:

  1. Verify that the following default value appears in the HEADER field, modifying the contents to match if necessary:

    {  "alg": "HS256",
      "typ": "JWT"
    }
  2. In the VERIFY SIGNATURE field, replace the value in the box (by default, secret) with nginx123. (You make this change before entering data in the PAYLOAD field to avoid a problem that occurs if you reverse the two steps.)

  3. Replace the contents of the PAYLOAD field with the following:

    {  "exp": 1545091200,
      "name": "Create New User",
      "sub": "cuser",
      "gname": "wheel",
      "guid": "10",
      "fullName": "John Doe",
      "uname": "jdoe",
      "uid": "222",
      "sudo": true,
      "dept": "IT",
      "url": "http://secure.example.com"
    }

    Note: The exp claim sets the JWT’s expiration date and time, representing it as a UNIX epoch time (the number of seconds since midnight UTC on January 1, 1970). The sample value represents midnight UTC on December 18, 2018. To adjust the expiration date, change the epoch time.

  4. Verify that the bar under the fields is blue and says Signature Verified.

  5. Copy the value in the left‑hand Encoded column into a file or buffer. It is the full text of the JWT that user jdoe needs to present in order to access http://secure.example.com, and we’ll use it in our testing below. We show the JWT with line breaks here for display purposes, but it must be presented to NGINX Plus as a single‑line string.

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDUwOTEyMDAsIm5hbWUiOi
    JDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW1lIjoid2hlZWwiLCJndWlkI
    joiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9lIiwidWlkIjoiMjIy
    Iiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5leGFtcGx
    lLmNvbSJ9.YYQCNvzj17F726QvKoIiuRGeUBl_xAKj62Zvc9xkZb4

Working on the NGINX Plus host, follow these steps to create the key file that NGINX Plus uses to verify JWTs that are signed with nginx123:

  1. Run this command to generate the Base64URL‑encoded string corresponding to the signature string. (The tr commands make the character substitutions required for Base64URL encoding.)

    # echo -n nginx123 | base64 | tr '+/' '-_' | tr -d '='bmdpbngxMjM
  2. In the /etc/nginx/ directory, create the key file called api_secret.jwk to be used by NGINX Plus to verify JWT signatures. Insert the following contents. The value in the k field is the Base64URL‑encoded form of nginx123, which we generated in the previous step.

    {"keys":    [{
            "k":"bmdpbngxMjM",
            "kty":"oct"
        }]
    }

Configuring NGINX Plus to Handle JWTs

The instructions in this section configure NGINX Plus to validate the JWT included in a request and to present a protected resource if the client is authorized (rather than the default page seen by unauthorized clients). We also define a new log format that captures JWT‑related information.

Configuring JWT Validation and Content‑Based Routing

In these instructions we’re following the standard best practice of renaming the default.conf configuration file so that NGINX Plus doesn’t read it, and creating a new configuration specifically for testing. This allows you to easily restore the default configuration when you’re done with testing or if there is a problem during testing.

  1. Rename default.conf:

    # mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak
  2. Create a new configuration file called jwt-test.conf in /etc/nginx/conf.d/, with the following contents. They configure JWT‑specific logging, JWT validation, and content‑based routing (a full analysis follows the snippet).

    server {
        listen       80;
        access_log  /var/log/nginx/host.access.log jwt;
    
        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
    
            # JWT validation
            auth_jwt "JWT Test Realm" token=$arg_myjwt;
            auth_jwt_key_file /etc/nginx/api_secret.jwk;
            error_log /var/log/nginx/host.jwt.error.log debug;
    
            if ( $jwt_claim_uid = 222 ) {
                add_header X-jwt-claim-uid "$jwt_claim_uid" always;
                add_header X-jwt-status "Redirect to $jwt_claim_url" always;
                return 301 $jwt_claim_url;
            }
    
            if ( $jwt_claim_uid != 222 ) {
                add_header X-jwt-claim-uid "$jwt_claim_uid" always;
                add_header X-jwt-status "Invalid user, no redirect" always;
            }
        }
    }

The directives in the location block tell NGINX Plus how to handle HTTP requests that include a JWT. (For information about the logging configuration defined by the access_log directive, see the next section.) NGINX Plus performs these steps:

  1. Extracts the JWT from the myjwt argument on the request string (as specified by the token argument to the auth_jwt directive).

  2. Decodes the JWT using the signing key specified by the auth_jwt_key-file directive (here, api_secret.jwk). It acts on the payload as follows (these actions are inherent to JWT processing and don’t have corresponding NGINX Plus directives):

    • Verifies that the JWT has not expired; that is, the expiration date specified by the exp claim in the payload is not in the past.
    • Creates a key‑value pair for each claim in the payload. The key name is a variable of the form $jwt_claim_claim-name (for example, $jwt_claim_uid for the uid claim).
  3. Logs any errors to /var/log/nginx/host.jwt.error.log at the debug level.

  4. Tests whether the value of $jwt_claim_uid is 222 (as specified by the two if directives) and sends the appropriate response to the client. This is how information in the JWT is used to perform content‑based routing.

    • If the value is 222, NGINX Plus sends a response that redirects the client (the return directive) to the URL specified in the JWt’s url claim. For debugging purposes, it adds two headers to the response (the add_header directives): the first captures the value of the uid claim and the second records the fact that the client was redirected.
    • If value is not 222, NGINX Plus serves the default index page (as defined by the root and index directives in the same location block). Again for debugging purposes, it adds headers that capture the value of the uid claim and record the fact that the client did not get access to the URL specified in the JWT.

    Note: Using the if directive to evaluate variables is not generally considered best practice, and we usually recommend using the map directive instead. For the purposes of this simple example, though, the if directive works as intended.

Effectively, the configuration provides access to protected resources only to authorized users. That is, users with a valid JWT get access to the URL specified in the JWT, while users without a valid JWT get access to a default page.

Logging JWT Data

We complete the configuration of JWT handling for content‑based routing by defining a logging format called jwt, which is referenced by the access_log directive in jwt-test.conf. It captures JWT data in the access log.

  1. Add the following log_format directive to /etc/nginx/nginx.conf:

    log_format jwt '$remote_addr - $remote_user [$time_local] "$request" '               '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
                   '$jwt_header_alg $jwt_claim_uid $jwt_claim_url';

    This format includes the two JWT claims that are used in this walkthrough (uid and url), but you can log any JWT claim data with the variable name corresponding to the claim, in the form $jwt_claim_claim‑name.

  2. Save nginx.conf, then run the following command to test the complete configuration (including the new jwt-test.conf file) for syntactic validity. Correct any reported errors.

    # nginx -t
  3. Reload NGINX Plus.

    # nginx -s reload

Testing the Configuration

Using a browser or a CLI tool like curl, we can test that NGINX Plus is correctly validating the JWT, authenticating the client who presents it, and performing content‑based routing. (To just test authentication and validation but not content‑based routing, comment out the two if blocks in jwt-test.conf.)

To run the test, we include the myjwt argument on the request URL, providing the full text of the JWT in which the url claim is 222. Again we’ve added line breaks for display purposes.

http://example.com/index.html?myjwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiO
jE1NDUwOTEyMDAsIm5hbWUiOiJDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW
1lIjoid2hlZWwiLCJndWlkIjoiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9
lIiwidWlkIjoiMjIyIiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5le
GFtcGxlLmNvbSJ9.YYQCNvzj17F726QvKoIiuRGeUBl_xAKj62Zvc9xkZb4

Here’s the corresponding curl command (without line breaks, so you can copy and paste it if you wish):

# curl -v example.com/index.html?myjwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDUwOTEyMDAsIm5hbWUiOiJDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW1lIjoid2hlZWwiLCJndWlkIjoiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9lIiwidWlkIjoiMjIyIiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5leGFtcGxlLmNvbSJ9.YYQCNvzj17F726QvKoIiuRGeUBl_xAKj62Zvc9xkZb4

Because the value of the uid claim in the JWT is 222, we expect NGINX Plus to display the contents of the restricted page, http://secure.example.com.

Now we test to verify that when the url claim in the JWT is not 222, NGINX Plus does not display the contents of the restricted page, but instead presents the index.html page of the local server, in this case http://example.com/index.html.

We start by generating another JWT at jwt.io with a uid claim other than 222; for the sake of example, we make it 111. Here’s the request URL with that JWT:

http://example.com/index.html?myjwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiO
jE1NDUwOTEyMDAsIm5hbWUiOiJDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW
1lIjoid2hlZWwiLCJndWlkIjoiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9
lIiwidWlkIjoiMTExIiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5l
eGFtcGxlLmNvbSJ9.Ch9xqsGzB8fRVX-3CBuCxP1Ia3oGKB1OnO6qwi_oBgg

The curl command is:

# curl -v example.com/index.html?myjwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDUwOTEyMDAsIm5hbWUiOiJDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW1lIjoid2hlZWwiLCJndWlkIjoiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9lIiwidWlkIjoiMTExIiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5leGFtcGxlLmNvbSJ9.Ch9xqsGzB8fRVX-3CBuCxP1Ia3oGKB1OnO6qwi_oBgg

In this case, we expect NGINX Plus to serve http://example.com/index.html.

In both test conditions, you can use a header‑inspection tool (such as curl or the developer tools provided with some browsers) to verify that the new headers, X-jwt-claim-uid and X-jwt-status, were added to the response.

If you have any issues during testing, check the access and error logs at /var/log/nginx/host.jwt*. The error log in particular reveals problems with verification, accessing your verification file, and so on.

Passing the JWT in a Cookie

In our basic example, NGINX Plus extracts the JWT from the myjwt argument on the request URL. NGINX Plus also supports passing the JWT in a cookie (for details, see the NGINX JWT reference documentation). In jwt-test.conf, change the auth_jwt directive so that the first element in the token parameter is $cookie instead of $arg:

auth_jwt "JWT Test Realm" token=$cookie_myjwt;

To provide the JWT in a cookie called myjwt, the appropriate curl command is:

# curl -v --cookie myjwt=JWT-text example.com/index.html

Try out content‑based routing with JWTs for yourself: start your free 30-day trial of NGINX today or contact us to discuss your uses cases.


"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."