Over a million developers have joined DZone.

Authenticating API Clients With JWT and NGINX Plus

This is the first in a six‑part series of blog posts that explore the new features in NGINX Plus R10 in depth. Let's start with authentication and JSON Web Tokens.

· Integration Zone

Is iPaaS solving the right problems? Not knowing the fundamental difference between iPaaS and dPaaS could cost you down the road. Brought to you in partnership with Liaison Technologies.

JSON Web Tokens (JWTs, pronounced “jots”) are a compact and highly portable means of exchanging identity information. The JWT specification has been an important underpinning of OpenID Connect, providing a single sign‑on token for the OAuth 2.0 ecosystem. JWTs can also be used as authentication credentials in their own right and are a better way to control access to web‑based APIs than traditional API keys.

With the release of NGINX Plus R10, NGINX Plus can validate JWTs directly. In this blog post, we describe how you can use NGINX Plus as an API gateway, providing a frontend to an API endpoint and using JWT to authenticate client applications.

Native JWT support is available only in NGINX Plus, not open source NGINX.

Anatomy of a JWT

JWTs have three parts: a header, a payload, and a signature. In transmission, they look like the following. We’ve added line breaks for readability (the actual JWT is a single string).

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
    eyJzdWIiOiJsYzEiLCJlbWFpbCI6ImxpYW0uY3JpbGx5QG5naW54LmNvbSIsImV4cCI6IjE0ODMyMjg3OTkifQ.
    VGYHWPterIaLjRi0LywgN3jnDUQbSsFptUw99g2slfc


As shown, a period ( . ) separates the header, payload, and signature. The header and payload are Base64‑encoded JSON objects, the encryption algorithm for the signature is specified by the alg header. When we decode our sample JWT we see:


EncodedDecoded
HeadereyJhbGciOiJIUzI1NiIsInR5cCI6Ik
 pXVCJ9
{
     "alg": "HS256",
     "typ": "JWT"
 }
PayloadeyJzdWIiOiJsYzEiLCJlbWFpbCI6Im
 xpYW0uY3JpbGx5QG5naW54LmNvbSIs
 ImV4cCI6IjE0ODMyMjg3OTkifQ
{
     "sub": "lc1",
     "email": "liam.crilly@nginx.com",
     "exp": "1483228799"
 }


The JWT standard defines several signature algorithms. The value HS256 in our example refers to HMAC SHA‑256, which we’re using for all sample JWTs in this blog post. NGINX Plus also supports the RS256 and EC256 signature algorithms that are defined in the standard. The ability to cryptographically sign JWTs makes them ideal for use as authentication credentials.

JWT as an API Key

A common way to authenticate an API client (the remote software client requesting API resources) is through a shared secret, generally referred to as an API key. A traditional API key is essentially a long and complex password that the client sends as an additional HTTP header on each and every request. The API endpoint grants access to the requested resource if the supplied API key is in the list of valid keys. Generally, the API endpoint does not validate API keys itself; instead, an API gateway handles the authentication process and routes each request to the appropriate endpoint. Besides computational offloading, this provides the benefits that come with a reverse proxy, such as high availability and load balancing to a number of API endpoints.

When authenticated access to APIs is controlled by traditional API keys, the API gateway validates the key presented by the API client by consulting a key registry, and passes the request to the appropriate API endpoint if the key is valid

API client authentication with a traditional API key

It is common to apply different access controls and policies to different API clients. With traditional API keys, this requires a lookup to match the API key with a set of attributes. Performing this lookup on each and every request has an understandable impact on the overall latency of the system. With JWT, these attributes are embedded, negating the need for a separate lookup.

Using JWT as the API key provides a high‑performance alternative to traditional API keys, combining best practice authentication technology with a standards‑based schema for exchanging identity attributes.

"Acting as an API gateway, NGINX Plus validates the JSON Web Token (JWT) presented by the API client and passes the request to the appropriate API endpoint if the JWT is valid

API client authentication with JWT and NGINX Plus

Configuring NGINX Plus as an Authenticating API Gateway

The NGINX Plus configuration for validating JWTs is very simple.

upstream api_server {

    server 10.0.0.1;

    server 10.0.0.2;

}
server {
   listen 80;
   location /products/ {
       auth_jwt "Products API";
       auth_jwt_key_file conf/api_secret.jwk;
       proxy_pass http://api_server;
   }
}

The first thing we do is specify the addresses of the servers that host the API endpoint in the upstream block. The location block specifies that any requests to URLs beginning with /products/ must be authenticated. The auth_jwt directive defines the authentication realm that will be returned (along with a 401 status code) if authentication is unsuccessful.

The auth_jwt_key_file directive tells NGINX Plus how to validate the signature element of the JWT. In this example we’re using the HMAC SHA‑256 algorithm to sign JWTs and so we need to create a JSON Web Key in conf/api_secret.jwk to contain the symmetric key used for signing. The file must follow the format described by the JSON Web Key specification; our example looks like this:

{"keys":

    [{

        "k":"ZmFudGFzdGljand0",

        "kty":"oct",

        "kid":"0001"

    }]

}

The symmetric key is defined by k and here is the Base64URL‑encoded value of the plaintext character string fantasticjwt. We obtained the encoded value by running this command:

$ echo -n fantasticjwt | base64 | tr '+\/' '-_' | tr -d '='

The "kty":"oct" pair defines the key type as a symmetric key (octet sequence). Finally, kid (Key ID) defines a serial number for this JSON Web Key, here 0001, which allows us to support multiple keys in the same file (named by the auth_jwt_key_file directive) and manage the lifecycle of those keys and the JWTs signed with them.

Now we are ready to issue JWTs to our API clients.

Issuing a JWT to API Clients

As an example API client, we’ll use a “quotation system” application and create a JWT for the API client. First, we define the JWT header:

{

    "typ":"JWT",

    "alg":"HS256",

    "kid":"0001"

}

"typ":"JWT" defines the type as JSON Web Token, "alg":"HS256" specifies that the JWT is signed with the HMAC SHA256 algorithm, and "kid":"0001" specifies that the JWT is signed with the JSON Web Key with that serial number.

Next we define the JWT payload:

{

  "name":"Quotation System",

  "sub":"quotes",

  "exp":"1577836800",

  "iss":"My API Gateway"

}

The sub (subject) field is our unique identifier for the full value in the name field. The exp field defines the expiration date in Unix Epoch time (the number of seconds since 1 January 1970). If this field is present in the payload, NGINX Plus checks the value as part of the JWT validation process and rejects expired JWTs even if they are otherwise correct. The iss field describes the issuer of the JWT, which is useful if your API gateway also accepts JWTs from third‑party issuers or a centralized identity management system.

Now we have everything we need to create the JWT, we follow these steps to correctly encode and sign it. Commands and encoded values appear on multiple lines only for readability; each one is actually typed as or appears on a single line:

  1. Separately flatten and Base64URL‑encode the header and payload.
    $ echo -n '{"typ":"JWT","alg":"HS256","kid":"0001"}' | base64 | tr '+\/' '-_' | tr -d '='
    
    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ
    $ echo -n '{"name":"Quotation System","sub":"quotes","exp":"1577836800","iss":"My API Gateway"}' | base64 | tr '+\/' '-_' | tr -d '='
     eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsImV4cCI6Ij
     E1Nzc4MzY4MDAiLCJpc3MiOiJNeSBBUEkgR2F0ZXdheSJ9
  2. Concatenate the encoded header and payload with a period (.) and assign the result to the HEADER_PAYLOAD variable.
    $ HEADER_PAYLOAD=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAw
    
    MDEifQ.eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsIm
    
    V4cCI6IjE1Nzc4MzY4MDAiLCJpc3MiOiJNeSBBUEkgR2F0ZXdheSJ9
  3. Sign the header and payload with our symmetric key and Base64URL‑encode the signature.
    $ echo -n $HEADER_PAYLOAD | openssl dgst -binary -sha256 -hmac fantasticjwt | base64 | tr '+\/' '-_' | tr -d '='
    
    McT4bZb8d8WlDgUQUl7rIEvhr3mQL8Faw_Qy1qfugrQ
  4. Append the encoded signature to the header and payload.
    $ echo $HEADER_PAYLOAD.McT4bZb8d8WlDgUQUl7rIEvhr3mQL8Faw_Qy1qfugrQ > quotes.jwt
  5. Test by making an authenticated request to the API gateway (in this example, the gateway is running on localhost).
    $ curl -H "Authorization: Bearer `cat quotes.jwt`" http://localhost/products/widget1

The curl command in Step 5 sends the JWT to NGINX Plus in the form of a Bearer Token, which is what NGINX Plus expects by default. NGINX Plus can also obtain the JWT from a cookie or query string parameter; to configure this, include the token= parameter to the auth_jwt directive. For example, with the following configuration NGINX Plus can validate the JWT sent with this curl command:

$ curl http://localhost/products/widget1?apijwt=`cat quotes.jwt`
server {

    listen 80;
    location /products/ {
        auth_jwt "Products API" token=$arg_apijwt;
        auth_jwt_key_file conf/api_secret.jwk;
        proxy_pass http://api_server;
    }
}

Once you’ve configured NGINX Plus, and generated and verified a JWT as shown above, you’re ready send the JWT to the API client developer and agree on the mechanism that will be used to submit the JWT with each API request.

Leveraging JWT Claims for Logging and Rate Limiting

One of the primary advantages of JWTs as authentication credentials is that they convey “claims”, which represent entities associated with the JWT and its payload (its issuer, the user to whom it was issued, and the intended recipient, for example). After validating the JWT, NGINX Plus has access to all of the reserved claims defined in the JWT standard, and captures them as variables that begin with $jwt_claim_ (for example, $jwt_claim_sub for the sub claim). This means that we can very easily proxy the information contained within the JWT to the API endpoint without needing to implement JWT processing in the API itself.

This configuration example shows some of the advanced capabilities.

log_format jwt '$remote_addr - $remote_user [$time_local] "$request" '

               '$status $body_bytes_sent "$http_referer" "$http_user_agent" '

               '$jwt_header_alg $jwt_claim_sub';
limit_req_zone $jwt_claim_sub zone=10rps_per_client:1m rate=10r/s;
server {
    listen 80;
        location /products/ {
        auth_jwt "Products API";
        auth_jwt_key_file conf/api_secret.jwk;
        limit_req zone=10rps_per_client;
        proxy_pass http://api_server;
        proxy_set_header API-Client $jwt_claim_sub;
        access_log /var/log/nginx/access_jwt.log jwt;
    }
}

The log_format directive defines a new format called jwt which extends the common log format with two additional fields, $jwt_header_alg and $jwt_claim_sub. Within the location block, we use the access_log directive to write logs with the values obtained from the validated JWT. The complete list of available variables is documented here.

In this example, were also using claim-based variables to provide API rate limiting per API client, instead of per IP address. This is particularly useful when multiple API clients are embedded in a single portal and cannot be differentiated by IP address. The limit_req_zone directive uses the JWT sub claim as the key for calculating rate limits, which are then applied to the location block by including the limit_req directive.

Finally, we provide the JWT subject as a new HTTP header when the request is proxied to the API endpoint. The proxy_set_header directive adds a HTTP header called API‑Client which the API endpoint can easily consume. Therefore the API endpoint does not need to implement any JWT processing logic. This becomes increasingly valuable as the number of API endpoints increases.

Revoking JWTs

From time to time it may be necessary to revoke or re‑issue an API client’s JWT. Using simple map and if blocks, we can deny access to an API client by marking its JWT as revoked until such time as the JWT’s exp claim (expiration date) is reached, at which point the map entry for that JWT can be safely removed.

map $jwt_claim_sub $jwt_status {
    "quotes" "revoked";
    "test" "revoked";
    default "";

}
server {
    listen 80;
    location /products/ {
        auth_jwt "Products API";
        auth_jwt_key_file conf/api_secret.jwk;
        if ( $jwt_status = "revoked" ) {
            return 403;
        }
        proxy_pass http://api_server;
    }
}

Summary

JSON Web Tokens are well suited to providing authenticated access to APIs. For the API client developer, they are just as easy to handle as traditional API keys, and they provide the API gateway with identity information that would otherwise require a database lookup. NGINX Plus provides support for JWT authentication and sophisticated configuration solutions based on the information contained within the JWT itself. Combined with other API gateway capabilities, NGINX Plus enables you to deliver API‑based services with speed, reliability, scalability, and security.

Discover the unprecedented possibilities and challenges, created by today’s fast paced data climate and why your current integration solution is not enough, brought to you in partnership with Liaison Technologies.

Topics:
native ,endpoint ,jwt ,keys ,api gateway ,client ,specification ,api ,developer ,support

Published at DZone with permission of Liam Crilly, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}