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:
- Token Server (Port 2000): Issues signed JWTs containing the correct scope.
- Protected API (Port 2001): Verifies tokens and enforces that the token contains the required
writescope.
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.jsonStep 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>
<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>
<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.shMembrane will now serve:
- Port 2000: Token issuing endpoint
- Port 2001: Protected API with scope validation
Step 5: Get a Token
Request a token from the token server:
curl http://localhost:2000
eyJhbGciOi...GyFAExplanation: 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:2001Expected 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>
<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:
- Request a new token (now without
writescope). - Use the token to call the protected API.
Expected result:
Access Denied!Next Steps
- Add additional custom claims (e.g.,
iss). - See How To: Forward a Custom Claim from JWT into HTTP Header.