How to secure APIs with JWT and Scope Validation

Overview

This guide demonstrates how to secure an API endpoint with JSON Web Tokens. Callers should not only be authenticated, but it should also be validated that the caller is in a role or has certain attributes.

To demonstrate the whole setup, a token server and an API to protect are needed. This tutorial describes how to set up both.

We will set up:

Step 1: Generate a JSON Web Key (JWK)

Why? The JWK is needed to sign and verify JWTs.

./membrane.sh generate-jwk -o ./conf/jwk.json

Step 2: Configure the Token Server

Add the following to your proxies.xml:

<api port="2000" name="Token Server">
    <request>
        <template>
        {
          "sub": "user@example.com",
          "aud": "order",
          "scp": "read write"
        }
        </template>
        <jwtSign>
            <jwk location="jwk.json"/>
        </jwtSign>
    </request>
    <return/>
</api>

Step 3: Configure the Protected API

Configure the API to require that write is present in the scp claim:

<api port="2001" name="Check Scope">
    <jwtAuth expectedAud="order">
        <jwks>
            <jwk location="jwk.json"/>
        </jwks>
    </jwtAuth>
    <if test="!exc.properties.jwt['scp'].contains('write')" language="groovy">
        <static>Access Denied!</static>
        <return statusCode="403"/>
    </if>
    <static>Access granted!</static>
    <return statusCode="200"/>
</api>

Step 4: Start Membrane

./membrane.sh

Membrane will now serve:

Step 5: Get a Token

Request a token from the token server:

curl http://localhost:2000

eyJhbGciOi...GyFA

Explanation: The token has scp: "read write", which meets the API’s requirement.

Optional: Inspect the Token

After retrieving the token, you can inspect it using jwt.io. Simply paste the token there to view its payload. You will notice that standard claims like iat (issued at), nbf (not before), and exp (expiration time) are automatically generated during the signing process. These ensure that the token has a valid lifetime and cannot be used outside the intended time window.

Step 6: Access the Protected API (Succeeds)

Use the token to call the protected API:

curl -H "Authorization: Bearer <your-token>" http://localhost:2001

Expected response:

Access granted!

Step 7: Remove "write" from the Token

Edit the token server configuration to remove the write scope:

<template>
{
  "sub": "user@example.com",
  "aud": "order",
  "scp": "read"
}
</template>

Restart Membrane so that the change takes effect.

Step 8: Test Again

Now simply repeat Step 5 and Step 6:

Expected result:

Access Denied!

Next Steps