Skip to content

Commit fc903f1

Browse files
authored
Use dynamo for encrypted cache (#186)
Lambda decryption performance completely wipes out all gains from using redis
1 parent b2cf6ac commit fc903f1

File tree

9 files changed

+41
-235
lines changed

9 files changed

+41
-235
lines changed

src/api/functions/encryption.ts

Lines changed: 0 additions & 81 deletions
This file was deleted.

src/api/functions/entraId.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ import { UserProfileData } from "common/types/msGraphApi.js";
3030
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
3131
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3232
import { checkPaidMembershipFromTable } from "./membership.js";
33-
import { getKey, setKey } from "./redisCache.js";
34-
import RedisClient from "ioredis";
3533
import type pino from "pino";
3634
import { type FastifyBaseLogger } from "fastify";
3735

@@ -41,16 +39,14 @@ function validateGroupId(groupId: string): boolean {
4139
}
4240

4341
type GetEntraIdTokenInput = {
44-
clients: { smClient: SecretsManagerClient; redisClient: RedisClient.default };
45-
encryptionSecret: string;
42+
clients: { smClient: SecretsManagerClient; dynamoClient: DynamoDBClient };
4643
clientId: string;
4744
scopes?: string[];
4845
secretName?: string;
4946
logger: pino.Logger | FastifyBaseLogger;
5047
};
5148
export async function getEntraIdToken({
5249
clients,
53-
encryptionSecret,
5450
clientId,
5551
scopes = ["https://graph.microsoft.com/.default"],
5652
secretName,
@@ -72,14 +68,13 @@ export async function getEntraIdToken({
7268
"base64",
7369
).toString("utf8");
7470
const cacheKey = `entra_id_access_token_${localSecretName}_${clientId}`;
75-
const cachedTokenObject = await getKey<{ token: string }>({
76-
redisClient: clients.redisClient,
77-
key: cacheKey,
78-
encryptionSecret,
79-
logger,
80-
});
81-
if (cachedTokenObject) {
82-
return cachedTokenObject.token;
71+
const startTime = Date.now();
72+
const cachedToken = await getItemFromCache(clients.dynamoClient, cacheKey);
73+
logger.debug(
74+
`Took ${Date.now() - startTime} ms to get cached entra ID token.`,
75+
);
76+
if (cachedToken) {
77+
return cachedToken.token as string;
8378
}
8479
const config = {
8580
auth: {
@@ -103,17 +98,13 @@ export async function getEntraIdToken({
10398
});
10499
}
105100
date.setTime(date.getTime() - 30000);
106-
if (result?.accessToken && result?.expiresOn) {
107-
await setKey({
108-
redisClient: clients.redisClient,
109-
key: cacheKey,
110-
data: JSON.stringify({ token: result.accessToken }),
111-
expiresIn:
112-
Math.floor(
113-
(result.expiresOn.getTime() - new Date().getTime()) / 1000,
114-
) - 120, // get new token 2 min before expiry
115-
encryptionSecret,
116-
});
101+
if (result?.accessToken) {
102+
await insertItemIntoCache(
103+
clients.dynamoClient,
104+
cacheKey,
105+
{ token: result?.accessToken },
106+
date,
107+
);
117108
}
118109
return result?.accessToken ?? null;
119110
} catch (error) {

src/api/functions/redisCache.ts

Lines changed: 6 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
1-
import { DecryptionError } from "common/errors/index.js";
21
import type RedisModule from "ioredis";
3-
import { z } from "zod";
4-
import {
5-
CORRUPTED_DATA_MESSAGE,
6-
decrypt,
7-
encrypt,
8-
INVALID_DECRYPTION_MESSAGE,
9-
} from "./encryption.js";
102
import type pino from "pino";
113
import { type FastifyBaseLogger } from "fastify";
124

135
export type GetFromCacheInput = {
146
redisClient: RedisModule.default;
157
key: string;
16-
encryptionSecret?: string;
178
logger: pino.Logger | FastifyBaseLogger;
189
};
1910

@@ -22,70 +13,31 @@ export type SetInCacheInput = {
2213
key: string;
2314
data: string;
2415
expiresIn?: number;
25-
encryptionSecret?: string;
16+
logger: pino.Logger | FastifyBaseLogger;
2617
};
2718

28-
const redisEntrySchema = z.object({
29-
isEncrypted: z.boolean(),
30-
data: z.string(),
31-
});
32-
3319
export async function getKey<T extends object>({
3420
redisClient,
3521
key,
36-
encryptionSecret,
3722
logger,
3823
}: GetFromCacheInput): Promise<T | null> {
24+
logger.debug(`Getting redis key "${key}".`);
3925
const data = await redisClient.get(key);
4026
if (!data) {
4127
return null;
4228
}
43-
const decoded = await redisEntrySchema.parseAsync(JSON.parse(data));
44-
if (!decoded.isEncrypted) {
45-
return JSON.parse(decoded.data) as T;
46-
}
47-
if (!encryptionSecret) {
48-
throw new DecryptionError({
49-
message: "Encrypted data found but no decryption key provided.",
50-
});
51-
}
52-
try {
53-
const decryptedData = decrypt({
54-
cipherText: decoded.data,
55-
encryptionSecret,
56-
});
57-
return JSON.parse(decryptedData) as T;
58-
} catch (e) {
59-
if (
60-
e instanceof DecryptionError &&
61-
(e.message === INVALID_DECRYPTION_MESSAGE ||
62-
e.message === CORRUPTED_DATA_MESSAGE)
63-
) {
64-
logger.info(
65-
`Invalid decryption, deleting old Redis key and continuing...`,
66-
);
67-
await redisClient.del(key);
68-
return null;
69-
}
70-
throw e;
71-
}
29+
return JSON.parse(data) as T;
7230
}
7331

7432
export async function setKey({
7533
redisClient,
7634
key,
77-
encryptionSecret,
7835
data,
7936
expiresIn,
37+
logger,
8038
}: SetInCacheInput) {
81-
const realData = encryptionSecret
82-
? encrypt({ plaintext: data, encryptionSecret })
83-
: data;
84-
const redisPayload: z.infer<typeof redisEntrySchema> = {
85-
isEncrypted: !!encryptionSecret,
86-
data: realData,
87-
};
88-
const strRedisPayload = JSON.stringify(redisPayload);
39+
const strRedisPayload = data;
40+
logger.debug(`Setting redis key "${key}".`);
8941
return expiresIn
9042
? await redisClient.set(key, strRedisPayload, "EX", expiresIn)
9143
: await redisClient.set(key, strRedisPayload);

src/api/plugins/auth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
159159
disableApiKeyAuth: boolean,
160160
): Promise<Set<AppRoles>> => {
161161
const { redisClient } = fastify;
162-
const encryptionSecret = fastify.secretConfig.encryption_key;
163162
const startTime = new Date().getTime();
164163
try {
165164
if (!disableApiKeyAuth) {
@@ -252,6 +251,7 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
252251
key: `jwksKey:${header.kid}`,
253252
data: JSON.stringify({ key: signingKey }),
254253
expiresIn: JWKS_CACHE_SECONDS,
254+
logger: request.log,
255255
});
256256
request.log.debug("Got JWKS signing key from server.");
257257
}
@@ -332,6 +332,7 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
332332
data: JSON.stringify([...userRoles]),
333333
redisClient,
334334
expiresIn: GENERIC_CACHE_SECONDS,
335+
logger: request.log,
335336
});
336337
request.log.debug("Retrieved user roles from database.");
337338
}

src/api/routes/iam.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
100100
clients: await getAuthorizedClients(),
101101
clientId: fastify.environmentConfig.AadValidClientId,
102102
secretName: genericConfig.EntraSecretName,
103-
encryptionSecret: fastify.secretConfig.encryption_key,
104103
logger: request.log,
105104
});
106105
await patchUserProfile(
@@ -166,7 +165,6 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
166165
clients: await getAuthorizedClients(),
167166
clientId: fastify.environmentConfig.AadValidClientId,
168167
secretName: genericConfig.EntraSecretName,
169-
encryptionSecret: fastify.secretConfig.encryption_key,
170168
logger: request.log,
171169
});
172170
const groupMembers = listGroupMembers(entraIdToken, groupId);
@@ -234,7 +232,6 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
234232
clients: await getAuthorizedClients(),
235233
clientId: fastify.environmentConfig.AadValidClientId,
236234
secretName: genericConfig.EntraSecretName,
237-
encryptionSecret: fastify.secretConfig.encryption_key,
238235
logger: request.log,
239236
});
240237
if (!entraIdToken) {
@@ -330,7 +327,6 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
330327
clients: await getAuthorizedClients(),
331328
clientId: fastify.environmentConfig.AadValidClientId,
332329
secretName: genericConfig.EntraSecretName,
333-
encryptionSecret: fastify.secretConfig.encryption_key,
334330
logger: request.log,
335331
});
336332
const groupMetadataPromise = getGroupMetadata(entraIdToken, groupId);
@@ -585,7 +581,6 @@ No action is required from you at this time.
585581
clients: await getAuthorizedClients(),
586582
clientId: fastify.environmentConfig.AadValidClientId,
587583
secretName: genericConfig.EntraSecretName,
588-
encryptionSecret: fastify.secretConfig.encryption_key,
589584
logger: request.log,
590585
});
591586
const response = await listGroupMembers(entraIdToken, groupId);
@@ -604,22 +599,24 @@ No action is required from you at this time.
604599
onRequest: fastify.authorizeFromSchema,
605600
},
606601
async (request, reply) => {
607-
const entraIdToken = await getEntraIdToken({
608-
clients: await getAuthorizedClients(),
609-
clientId: fastify.environmentConfig.AadValidClientId,
610-
secretName: genericConfig.EntraSecretName,
611-
encryptionSecret: fastify.secretConfig.encryption_key,
612-
logger: request.log,
613-
});
614602
const { redisClient } = fastify;
615603
const key = `entra_manageable_groups_${fastify.environmentConfig.EntraServicePrincipalId}`;
616604
const redisResponse = await getKey<{ displayName: string; id: string }[]>(
617605
{ redisClient, key, logger: request.log },
618606
);
619607
if (redisResponse) {
620608
request.log.debug("Got manageable groups from Redis cache.");
621-
return reply.status(200).send(redisResponse);
609+
return reply
610+
.header("X-ACM-Data-Source", "redis")
611+
.status(200)
612+
.send(redisResponse);
622613
}
614+
const entraIdToken = await getEntraIdToken({
615+
clients: await getAuthorizedClients(),
616+
clientId: fastify.environmentConfig.AadValidClientId,
617+
secretName: genericConfig.EntraSecretName,
618+
logger: request.log,
619+
});
623620
// get groups, but don't show protected groups as manageable
624621
const freshData = (
625622
await getServicePrincipalOwnedGroups(
@@ -639,6 +636,7 @@ No action is required from you at this time.
639636
key,
640637
data: JSON.stringify(freshData),
641638
expiresIn: GENERIC_CACHE_SECONDS,
639+
logger: request.log,
642640
});
643641
return reply.status(200).send(freshData);
644642
},

src/api/routes/membership.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
112112
clients: await getAuthorizedClients(),
113113
clientId: fastify.environmentConfig.AadValidClientId,
114114
secretName: genericConfig.EntraSecretName,
115-
encryptionSecret: fastify.secretConfig.encryption_key,
116115
logger: request.log,
117116
});
118117
const paidMemberGroup = fastify.environmentConfig.PaidMemberGroupId;
@@ -233,7 +232,6 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
233232
clients: await getAuthorizedClients(),
234233
clientId: fastify.environmentConfig.AadValidClientId,
235234
secretName: genericConfig.EntraSecretName,
236-
encryptionSecret: fastify.secretConfig.encryption_key,
237235
logger: request.log,
238236
});
239237
const paidMemberGroup = fastify.environmentConfig.PaidMemberGroupId;

0 commit comments

Comments
 (0)