en
                    array(1) {
  ["en"]=>
  array(13) {
    ["code"]=>
    string(2) "en"
    ["id"]=>
    string(1) "1"
    ["native_name"]=>
    string(7) "English"
    ["major"]=>
    string(1) "1"
    ["active"]=>
    string(1) "1"
    ["default_locale"]=>
    string(5) "en_US"
    ["encode_url"]=>
    string(1) "0"
    ["tag"]=>
    string(2) "en"
    ["missing"]=>
    int(0)
    ["translated_name"]=>
    string(7) "English"
    ["url"]=>
    string(78) "https://www.statworx.com/en/content-hub/blog/two-patterns-to-secure-rest-apis/"
    ["country_flag_url"]=>
    string(87) "https://www.statworx.com/wp-content/plugins/sitepress-multilingual-cms/res/flags/en.png"
    ["language_code"]=>
    string(2) "en"
  }
}
                    
Contact
Content Hub
Blog Post

Two Patterns To Secure REST APIs

  • Expert Andre Münch
  • Date 08. July 2020
  • Topic Data EngineeringData Science
  • Format Blog
  • Category Technology
Two Patterns To Secure REST APIs

In this blog post, I want to present two models of how to secure a REST API. Both models work with JSON Web Tokens (JWT). We suppose that token creation has already happened somewhere else, e.g., on the customer side. We at STATWORX are often interested in the verification part itself.

The first model extends a running unsecured interface without changing any code in it. This model does not even rely on Web Tokens and can be applied in a broader context. The second model demonstrates how token verification can be implemented inside a Flask application.

About JWT

JSON Web Tokens are more or less JSON objects. They contain authority information like the issuer, the type of signing, information on their validity span, and custom information like the username, user groups and further personal display data. To be more precise: it consists of three parts: the header, the payload, and the signature. All three parts are JSON objects, are base64url encoded and put together in the following fashion, where the dots separate the three parts:

base64url(header).base64url(payload).base64url(signature)

All information is transparent and readable to whoever the token has (just decode the parts). Its security and validity stem from its digital signing, the signing guarantees that a Web Token is from its issuer.

There are two different ways of digital signing: with a secret or with a public-private-key pair in various forms and algorithms. This choice also influences the verification possibilities: The symmetric algorithm with a password can only be verified by the owner of the password – the issuer – whereas, for asymmetric algorithms, the public part can be distributed and, therefore, used for verification.

Besides this feature, JWT offers additional advantages and features. Here is a brief overview

  • Decoupling: The application or the API will not need to implement a secure way to exchange passwords and verify them against a backend.
  • Purpose oriented: Tokens can be issued for a particular purpose (defined within the payload) and are only valid for this purpose.
  • Less password exchange: Tokens can be used multiple times without password interactions.
  • Expiration: Tokens expire. Even in the event of theft and criminal intent, the amount of information that can be obtained is limited to its validity span.

More information about JWT and also a debugger can be found at https://jwt.io

Authorization Header

Moving over to consideration on how Authorization is exchanged within HTTP requests.

To authenticate against a server, you can use the Request Header Authorization, of which various types exist. Down below are two common examples:

  • Basic
  Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

where the latter part is a base64 encoded string of user:password

  • Bearer
  Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpbmZvIjoiSSdtIGEgc2lnbmVkIHRva2VuIn0.rjnRMAKcaRamEHnENhg0_Fqv7Obo-30U4bcI_v-nfEM

where the latter part is a JWT.

Note that this information is totally transparent on transfer unless HTTPS protocol is used.

Pattern 1 – Sidecar Verification

Let’s start with the first pattern that I’d like to introduce.

In this section, I’ll introduce a setup with nginx and use its feature of sub-requests. This is especially useful in cases where the implementation of an API can not be changed.

Nginx is a versatile tool. It acts as a web server, a load balancer, a content cache, and a reversed proxy. With the latest Web Server Survey of April 2020, nginx is the most used web server for public web sites.

By using nginx and sub-requests, each incoming request has to go through nginx, and the header, including the Authorization part, is passed to a sub-module (calling it Auth Service). This sub-module then checks the validity of the Authorization before sending it through to the actual REST API or declining it. This decision process depends on the following status codes of the Auth Service:

  • 20x: nginx passes the request over to the actual resource service
  • 401 or 403: nginx denies the access and sends the response from the authentication service instead

As you might have noticed, this setup does not exclusively build on JWTs, and therefore other authorization types can be used.

Sub-request model.

Schema of request handling: 1) pass over to Auth Service for verification, 2) pass over to REST API if the verification was successful.

Nginx configuration

Two configuration amendments need to be taken to have nginx configured for token validation:

  • Add an (internal) directive to the authorization service which verifies the request
  • Add authentication parameters to the directive that needs to be secured

The authorization directive is configured like this

location /auth {
        internal;
        proxy_pass                          http://auth-service/verify;
        proxy_pass_request_body off;
        proxy_set_header                Content-Length "";
        proxy_set_header                X-Original-URI $request-uri;
}

This cuts off the body and sends the rest of the request to the authorization service.

Next, the configuration of the directive which forwards to your REST API:

location /protected {
        auth_request    auth/;
        proxy_pass      http://rest-service;
}

Note that auth_request points to the authorization directive we have set up before.

Please see the Nginx documentation for additional information on the sub-request pattern.

Basic Implementation of the Auth Service

In the sub-module, we use the jwcrypto library for the token operations. It offers all features and algorithms needed for the authentication task. Many other libraries can do similar things, which can be found at Jwt.io.

Suppose the token was created by an asymmetric cryptographical algorithm like RSA, and you have access to the public key (here called: public.pem)

Following our configuration on nginx, we will add the directive /verify that does the verification job:

from jwcrypto import jwt, jwk
from flask import Flask

app = Flask(__name__)

# Load the public key
with open('public.pem', 'rb') as pemfile:
        public_key = jwk.JWK.from_pem(pemfile.read())

def parse_token(auth_header):
      # your implementation of Authorization Header extraction
    pass

@app.route("/verify")
def verify():
      token_raw = parse_token(request.headers["Authorization"])
      try:
            # decode token
        token = jwt.JWT(jwt=token_raw, key=public_key)
        return 200, "this is a secret information"
    except:
          return 403, ""

The script consists of three parts: Reading the public key with the start of the API, extracting the header information (not given here), and the actual verification that is embedded in a try-catch expression.

Pattern 2 – Verify within the API

In this section, we will implement the verification within our Flask API.

There are packages in the Flask universe available like flask_jwt; however, they do not offer the full scope of features and, in particular, not for our case here. Instead, we again use the library jwcrypto like before.

It is further assumed that you have access to the public key again, but this time, a specific directive is secured (here called: /secured ) – and also, access should only be granted to admin users.

from jwcrypto import jwt, jwk
from flask import Flask

app = Flask(__name__)

# Load the public key
with open('public.pem', 'rb') as pemfile:
        public_key = jwk.JWK.from_pem(pemfile.read())


@app.route("/secured")
def secured():
      token_raw = parse_token(request.headers["Authorization"])
      try:
            # decode token
        token = jwt.JWT(jwt=token_raw, key=public_key)
        if "admin" in token.claims["groups"]:
              return 200, "this is a secret information"
        else:
              return 403, ""
    except:
          return 403, ""

The setup is the same as in the previous example, but an additional check was added: whether the user is part of the admin group.

Summary

Two patterns were discussed, which both use Web Tokens to authenticate the request. While the first pattern’s charm lies in the decoupling of Authorization and API functionality, the second approach is more compact. It perfectly fits situations where the number of APIs is low, or the overhead caused by a separate service is too high. Instead, the sidecar pattern is perfect when you provide several APIs and like to unify the Authorization in one separate service.

Web Tokens are used in popular authorization schemes like OAuth and OpenID Connect. In another blog post, I will focus on how they work and how they connect to the verification I presented.

I hope you have enjoyed the post. Happy coding!

Andre Münch Andre Münch

Learn more!

As one of the leading companies in the field of data science, machine learning, and AI, we guide you towards a data-driven future. Learn more about statworx and our motivation.
About us