Skip to content
All articles
auth5 min read

How to use Redis as a auth decision cache

Originally published on Medium

Let’s undertsand the problem first:

While building the Authorisation systems specially the one who checks the authorisation for every single resource in our product. It means for every single backed route we have to check for the permission like

‘Doese a user id 001 has create acess for a product list ? ‘

Auth service will check for the 3 things as
User + Resource + Action

like : user001 : product : create_product

Now let’s see, what are the problems we are facing and how we will approach it.

Problem 1:

This is how for every single request we will be keep calling the Auth service and do an expensive database query and get the permissions as allow the acess or deny.

Solution 1:

To avoid such repetative Computes we use to cache such decisions / permissions for users in a in memeory database like redis.
It

  1. Reduces lookup time
  2. Reduces compute cost
  3. Avoids recompute of same decision again and again

Implementation:

We will place the redis between the ingestor (which is asking for permissions decision) and Auth service ( a decision maker )

Fig 1: Auth decisions cached using Redis As a cache

To store the decisions in cache, we can construct a Redis key from:

user + action + resource

So:

user      = 001
action = product:create
resource = product-list

can become:

authz:iam:001:product:create:product-list

We can then store:

1 → ALLOW 0 → DENY

Code :



const keyPrefix = (userId: string): string =>
`authz:iam:${userId}`;

const cacheKey = (
userId: string,
action: string,
resource: string,
): string =>
`${keyPrefix(userId)}:${action}:${resource}`;

Now we can first check Redis before calling the authorization service.

The check() function below represents the actual authorization-service call.

export async function cachedCheck( 
userId: string,
action: string,
resource: string,
): Promise<CachedCheckResult> {

const key = cacheKey(userId, action, resource);

try {

const hit = await redis.get(key);

if (hit === '1' || hit === '0') {

return { allowed: hit === '1',
source: 'iam',
cached: true,
};
}
}
catch (error) {
// Redis failure should not become an authorization failure.

}

const result = await check(userId, action, resource);

await write(key, result); //storing into redis

return {
...result,
cached: false,
};
}

Plus

At this Moment this moment it might be look like we have solved the issue right ?

partially correct, we have solved the most fundamental problem of recompute by caching but what if……

Problem 2 :

You have been changed a Role or a permission attached to a user but now your Backend will Look into the stale cache data. and might deliver the stale auth decisions.

Stale auth decisions in Redis causing Decision Permision mismatch

Solution 2 :

To Solve this issue of stale cache on the update in permission we will call out the redis and will drop the permissions cached for a user for which we have updated the permissions..

So the next time, when the backend needs a auth decison — -> It will miss the cache and — -> Hit auth Service — -> Get decision + Update the cache..

Implementation:

we will use the user id as a key to store the permissions for that user id in the cache and will drop that entries having key == user_id

For example: Redis Keys will look like ..

authz:iam:001:product:create:product-list
authz:iam:001:product:delete:product-list
authz:iam:001:project:read:project-123

When the permissions of user 001 change, we want to remove all keys beginning with:

authz:iam:001:

Code :

const keyPrefix = (userId: string): string =>
`authz:iam:${userId}`;

We can then invalidate all cached decisions for that user.

export async function invalidateUser(
userId: string,
): Promise<void> {
const match = `${keyPrefix(userId)}:*`;

let cursor = '0';
do {
const [next, keys] = await redis.scan(
cursor,
'MATCH',
match,
'COUNT',
200,
);
cursor = next;
if (keys.length > 0) {
await redis.del(...keys);
}
} while (cursor !== '0');
}

Notice that we use Redis SCAN rather than KEYS.

Using KEYS against a large Redis database can block Redis while the keyspace is scanned. SCAN allows us to incrementally iterate over matching keys.

Now our permission-update flow can call:

await invalidateUser(userId);

after changing the user’s roles or bindings.Currently, Above implementation will solve our problem 2.

Now at this point all Looks good,

  1. We are caching auth decisions → reduced network calls
  2. We are updating the cache on the changing the permissions for a users

But Still One Problem is Remaining ..

Problem 3:

What if the Auth service is down / unreachable and cache doesn’t have the requested auth decision cached…

At that moment our cache will should not be deny / allow the permission by it self. It should be handel such service failures gracefully.

Therefore, service unavailability should not be treated as an authorization decision.

Solution 3 :

Redis should not invent an authorization decision when the authorization service is unavailable. Instead, the authorization layer should return a special state such as:

source = unavailable

The backend can then decide how that failure should be represented to the client — for example, as a 503 Service Unavailable, depending on the application's failure policy.

Most importantly:

Do not cache the unavailable result.

Imagine the authorization service is unavailable for only 10 seconds.

If we cache: DENY

Then for next few minutes / Time for which cache lives , then a temporary 10-second outage becomes a huge authorization outage for that user.This time varies as per the TTL time you have been set for your cache.

Implementation 3:

First, the authorization check can return a result that distinguishes a real decision from a service failure.

For example:

export type CheckResult = {
allowed: boolean;
source: 'iam' | 'unavailable';
};

Then, when writing the result to Redis, explicitly skip unavailable results like ‘unavailable’.
Code

async function write(
key: string,
result: CheckResult,
): Promise<void> {
// Never cache an authorization-service outage.
if (result.source === 'unavailable') {
return;
}

await redis.setex(
key,
config.authzCacheTtlSeconds,
result.allowed ? '1' : '0',
);
}

Adding a TTL as a Safety Net

Even with explicit cache invalidation, it is still useful to configure a TTL.Because not every permission change will necessarily pass through the same invalidation mechanism.

For example, you may have:

  • manual database changes
  • administrative scripts
  • migration jobs
  • backfill scripts
  • unexpected update paths

The TTL provides a final consistency backstop.

For example: TTL = 60 seconds

means that even if an invalidation event is missed, the cached authorization decision will eventually disappear.

So, That’s all about the using redis as a Auth cache..

If you want to read such blogs, written with this approach you can follow Rohit Mane.
Thank You for Attention!!
Make sure to follow me for Such Interesting Dev Stories.
Socials :
X
Portfolio
Github
LinkedIn
#auth#redis#authorisation#backend#production