DEV Community

Sedky Abou-Shamalah
Sedky Abou-Shamalah

Posted on • Updated on

Let's stop using JWTs client side

Here's an updated version of this blog:
https://sedkodes.com/blog/lets-stop-using-jwts

Split Token Flow: A solution to the security problem of the JWT

OAuth2, OIDC, and their foundation, the JWT, has been an industry standard for many years, with no sign of slowing down.

The problem with the JWT is the inherent leakiness. There is a massive debate amongst the community - where some consider JWT for auth as insecure as it leaks information (by b64 decoding the body), others argue that you shouldn't put sensitive info in there at all.

So that’s where the split token flow comes in. This flow suggests to use just the signature of the JWT access token on the client side, and storing the header and claims of the JWT server side. Thus, the split token flow satisfies both camps - we get the flexibility of JWTs by being able to store session information in JWT claims, and we get the security of an Opaque access token - because we don't actually expose the entire token, only the signature.

How can this be achieved with an API Gateway?

Full Disclosure, I am an employee of Tyk. The following guide contains screenshots of Tyk Pro product. However, all of this functionality is completely free to implement using the OSS Gateway as well. In fact, you can browse to this repository which will provide a step-by-step guide to doing the rest with the OSS Tyk Gateway.

First, let’s take an example of client credentials flow, where we exchange a client id and secret for a JWT access token that we can use to access our APIs:

$ curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' https://keycloak-host/auth/realms/tyk/protocol/openid-connect/token \
-d grant_type=client_credentials \
-d client_id=efd952c8-df3a-4cf5-98e6-868133839433 \
-d client_secret=0ede3532-f042-4120-bece-225e55a4a2d6 \
> -s | jq
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbWFpbCI6ImhlbGxvQHdvcmxkLmNvbSJ9.EwIaRgq4go4R2M2z7AADywZ2ToxG4gDMoG4SQ1X3GJ0",
  "expires_in": 300,
  "token_type": "bearer",
  "not-before-policy": 0,
  "scope": "email profile"
Enter fullscreen mode Exit fullscreen mode

So here we get a JWT access token back:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbWFpbCI6ImhlbGxvQHdvcmxkLmNvbSJ9.EwIaRgq4go4R2M2z7AADywZ2ToxG4gDMoG4SQ1X3GJ0
Enter fullscreen mode Exit fullscreen mode

Header

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Enter fullscreen mode Exit fullscreen mode

Body

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbWFpbCI6ImhlbGxvQHdvcmxkLmNvbSJ9
Enter fullscreen mode Exit fullscreen mode

Signature

EwIaRgq4go4R2M2z7AADywZ2ToxG4gDMoG4SQ1X3GJ0
Enter fullscreen mode Exit fullscreen mode

We can plug that into jwt.io and see the decoded payload:

Alt Text

Clearly, the access token can contain sensitive information that we don’t want to leak.

The API Gateway is perfectly positioned to act as a broker between the client and the authorization server.

It can intercept requests from clients, receive a client id and secret, and exchange that for an access token with the authorization server. In this broker position, the API gateway can break apart the JWT, and return only the signature portion of the real JWT access token back to the client. We then store the rest of the JWT as metadata.

Setting it Up

Inside Tyk, let’s create a virtual endpoint or API, listening on the path "/auth/token".

Let’s take a look at some sample code for the Virtual Endpoint:

function login(request, session, config) {
    var credentials = request.Body.split("&")
        .map(function(item, index) {
            return item.split("=");
      }).reduce(function(p, c) {
             p[c[0]] = c[1];
             return p;
      }, {});

    var newRequest = {
      "Headers": {"Content-Type": "application/x-www-form-urlencoded"},
      "Method": "POST",
      "FormData": {
          grant_type: credentials.grant_type,
          client_id: credentials.client_id,
          client_secret: credentials.client_secret
      },
      "Domain": "https://keycloak-host",
      "resource": "/auth/realms/tyk/protocol/openid-connect/token",
    };

    var response = TykMakeHttpRequest(JSON.stringify(newRequest));
    var usableResponse = JSON.parse(response);

    if (usableResponse.Code !== 200) {
      return TykJsResponse({
        Body: usableResponse.Body,
        Code: usableResponse.Code
      }, session.meta_data)
    }

    var bodyObj = JSON.parse(usableResponse.Body);
    var accessTokenComplete = bodyObj.access_token;
    var signature = accessTokenComplete.split(".")[2];

    log("completeAccessToken: " + accessTokenComplete);

    // create key inside Tyk
    createKeyInsideTyk(signature, bodyObj)

    // override signature
    bodyObj.access_token = signature;
    delete bodyObj.refresh_expires_in;
    delete bodyObj.refresh_token;
    delete bodyObj.foo;

  var responseObject = {
    Body: JSON.stringify(bodyObj),
    Code: usableResponse.Code
  }
  return TykJsResponse(responseObject, session.meta_data)
}

function createKeyInsideTyk(customKey, meta) {
    // TODO: this needs to be a bit more dynamic. e.g. work out the policy id & API ID etc... based on the metadata
    var accessRights = {
        "c399587af48441d17bc5700339aa34fa": {
            "api_name": "Test API",
            "api_id": "c399587af48441d17bc5700339aa34fa",
            "versions": [
                "Default"
            ]
        }
    }

    log("meta: " + JSON.stringify(meta));

    var keyRequestBody = keyRequestTemplate;
    keyRequestBody.access_rights = accessRights;

    var newRequest = {
      "Headers": {"Content-Type": "application/json", "Authorization": "Bearer a4fcbde85a3c477d424922990eb16e01"},
      "Method": "POST",
      "Body": JSON.stringify(keyRequestBody),
      "Domain": "http://localhost:3000",
      "resource": "/api/keys/" + customKey,
    };

    var response = TykMakeHttpRequest(JSON.stringify(newRequest));
    log("createkeyintykres: " + response);
}

var keyRequestTemplate = {
    "apply_policies": [],
    "org_id" : "5d67b96d767e02015ea84a6f",
    "expires": 0,
    "allowance": 0,
    "per": 0,
    "quota_max": 0,
    "rate": 0,
    "access_rights": {}
}
Enter fullscreen mode Exit fullscreen mode

The code does the following:

  1. The Virtual Endpoint receives a request containing a client ID + Secret
  2. It forwards the request to the authorization server and receives the access JWT token
  3. It splits the access token, creating an opaque key in Tyk, which is the signature of the JWT access token
  4. It adds the Header and the Body of the access token as metadata to that opaque key so that we can look it up on subsequent requests
  5. It returns the opaque key (signature) to the client where they can use it to access APIs

At the beginning, from the client perspective:

$ curl http://tyk-gw:8080/auth/token -X POST -H 'Content-Type: application/x-www-form-urlencoded' -d client_id=efd952c8-df3a-4cf5-98e6-868133839433 -d client_secret=0ede3532-f042-4120-bece-225e55a4a2d6 -d grant_type=client_credentials

{"access_token":"MEwIaRgq4go4R2M2z7AADywZ2ToxG4gDMoG4SQ1X3GJ0","expires_in":300,"not-before-policy":0,"scope":"email profile","session_state":"fb8754d1-d518-40e8-a84f-85347a0639c8","token_type":"bearer"}
Enter fullscreen mode Exit fullscreen mode

Let's look up the opaque token

Alt Text

And the key’s meta data

Alt Text

Let’s test our API key against the API we added to the access rights in the Create Key payload:

$ curl localhost:8080/basic-protected-api/get -H "Authorization: MEw….GJ0"
{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip",
    "Authorization": "MEwIaRgq4go4R2M2z7AADywZ2ToxG4gDMoG4SQ1X3GJ0",
    "Host": "httpbin",
    "User-Agent": "curl/7.64.1"
  },
  "origin": "192.168.80.1",
  "url": "http://httpbin/get"
}
Enter fullscreen mode Exit fullscreen mode

Tyk validates the opaque token and allows access to the API.

In Tyk, let's reconstruct the full JWT and safely pass it to our upstream API.

In the previous step, we stored the full JWT in the session token’s metadata inside Tyk. We can extract the JWT from the request’s session metadata and then inject it into the requests headers.

Let’s configure the API to inject a global header like so:

Alt Text

Let’s try the API call again:

$ curl localhost:8080/basic-protected-api/get -H "Authorization: MEw….GJ0"
{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip",
    "Authorization": "Bearer eyJh...1X3GJ0",
    "Host": "httpbin",
    "User-Agent": "curl/7.64.1"
  },
  "origin": "192.168.80.1",
  "url": "http://httpbin/get"
}
Enter fullscreen mode Exit fullscreen mode

Tada!
As you can see, even though we only sent an opaque token in the request, Tyk injected the rest of the JWT where our upstream can now use it to perform business logic.

If you have any questions or want to start a conversation, I encourage you to email me at sedky@tyk.io with any questions.

Oldest comments (0)