Let’s stop using JWTs client side

Sedky Abou-Shamalah
6 min readDec 8, 2020

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. On the contrary, the OAuth RFC continues to be iteratively improved, aligning with FHIR and Open Banking principles.

All that to say, we should expect the OAuth flow to continue to reign supreme.

There are two types of access tokens in the OAuth flow, opaque and JWT (JWS more precisely). 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.

maybe your face at this time

Introduce: the split token flow comes in. This flow suggests to use 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.

No more words!! How can this be achieved with Tyk?

Note, 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://my-auth-server/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”

So here we get a JWT access token back:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbWFpbCI6ImhlbGxvQHdvcmxkLmNvbSJ9.EwIaRgq4go4R2M2z7AADywZ2ToxG4gDMoG4SQ1X3GJ0

Header

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Body

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbWFpbCI6ImhlbGxvQHdvcmxkLmNvbSJ9

Signature

EwIaRgq4go4R2M2z7AADywZ2ToxG4gDMoG4SQ1X3GJ0

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

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

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

Tyk 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, retrievable using the signature as a key inside Tyk.

Ok really, how?

Inside Tyk, let’s create a virtual endpoint, listening on the path /token. This virtual endpoint executes the “login” flow.

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://my-auth-server”,
“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”: {}
}

The code does the following:

1. The Virtual Endpoint receives a request containing a client ID + Secret
2. It makes a call to the authorization server, 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

From the client perspective, when logging in at the token endpoint:

$ 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”}

Notice that the returned response is considerably smaller than before. As it’s just the signature of the access_token field.

If we are on Tyk Pro, we can even look up the key in the Dashboard:

And the key’s meta data

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: MEwIaRgq4go4R2M2z7AADywZ2ToxG4gDMoG4SQ1X3GJ0”{
“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
}

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

What if our APIs need the access token?

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:

This will instruct Tyk to combine the JWT Header and Body from the session metadata, with the opaque token that the client is using to make an API request, and inject that new value as an Authorization header.

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
}

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.

All this is possible on the OSS version of Tyk, as evident here.

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

--

--