Skip to content

Commit c1b80e2

Browse files
committed
replace all getEntraToken with redis version
1 parent 996be8c commit c1b80e2

File tree

10 files changed

+184
-73
lines changed

10 files changed

+184
-73
lines changed

src/api/functions/encryption.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ const KEY_LEN = 32;
77
const ALGORITHM = "aes-256-gcm";
88
const HASH_FUNCTION = "sha512";
99

10+
export const INVALID_DECRYPTION_MESSAGE =
11+
"Could not decrypt data (check that the encryption secret is correct).";
12+
1013
export function encrypt({
1114
plaintext,
1215
encryptionSecret,
@@ -62,8 +65,7 @@ export function decrypt({
6265
]);
6366
} catch (e) {
6467
throw new DecryptionError({
65-
message:
66-
"Could not decrypt data (check that the encryption secret is correct).",
68+
message: INVALID_DECRYPTION_MESSAGE,
6769
});
6870
}
6971

src/api/functions/entraId.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "../../common/config.js";
1010
import {
1111
BaseError,
12+
DecryptionError,
1213
EntraFetchError,
1314
EntraGroupError,
1415
EntraGroupsFromEmailError,
@@ -29,18 +30,32 @@ import { UserProfileData } from "common/types/msGraphApi.js";
2930
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
3031
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3132
import { checkPaidMembershipFromTable } from "./membership.js";
33+
import { getKey, setKey } from "./redisCache.js";
34+
import RedisClient from "ioredis";
35+
import type pino from "pino";
36+
import { type FastifyBaseLogger } from "fastify";
3237

3338
function validateGroupId(groupId: string): boolean {
3439
const groupIdPattern = /^[a-zA-Z0-9-]+$/; // Adjust the pattern as needed
3540
return groupIdPattern.test(groupId);
3641
}
3742

38-
export async function getEntraIdToken(
39-
clients: { smClient: SecretsManagerClient; dynamoClient: DynamoDBClient },
40-
clientId: string,
41-
scopes: string[] = ["https://graph.microsoft.com/.default"],
42-
secretName?: string,
43-
) {
43+
type GetEntraIdTokenInput = {
44+
clients: { smClient: SecretsManagerClient; redisClient: RedisClient.default };
45+
encryptionSecret: string;
46+
clientId: string;
47+
scopes?: string[];
48+
secretName?: string;
49+
logger: pino.Logger | FastifyBaseLogger;
50+
};
51+
export async function getEntraIdToken({
52+
clients,
53+
encryptionSecret,
54+
clientId,
55+
scopes = ["https://graph.microsoft.com/.default"],
56+
secretName,
57+
logger,
58+
}: GetEntraIdTokenInput) {
4459
const localSecretName = secretName || genericConfig.EntraSecretName;
4560
const secretApiConfig =
4661
(await getSecretValue(clients.smClient, localSecretName)) || {};
@@ -56,12 +71,15 @@ export async function getEntraIdToken(
5671
secretApiConfig.entra_id_private_key as string,
5772
"base64",
5873
).toString("utf8");
59-
const cachedToken = await getItemFromCache(
60-
clients.dynamoClient,
61-
`entra_id_access_token_${localSecretName}_${clientId}`,
62-
);
63-
if (cachedToken) {
64-
return cachedToken.token as string;
74+
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;
6583
}
6684
const config = {
6785
auth: {
@@ -85,13 +103,14 @@ export async function getEntraIdToken(
85103
});
86104
}
87105
date.setTime(date.getTime() - 30000);
88-
if (result?.accessToken) {
89-
await insertItemIntoCache(
90-
clients.dynamoClient,
91-
`entra_id_access_token_${localSecretName}`,
92-
{ token: result?.accessToken },
93-
date,
94-
);
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: result.expiresOn.getTime() - new Date().getTime() - 3600,
112+
encryptionSecret,
113+
});
95114
}
96115
return result?.accessToken ?? null;
97116
} catch (error) {

src/api/functions/redisCache.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { DecryptionError } from "common/errors/index.js";
22
import type RedisModule from "ioredis";
33
import { z } from "zod";
4-
import { decrypt, encrypt } from "./encryption.js";
4+
import { decrypt, encrypt, INVALID_DECRYPTION_MESSAGE } from "./encryption.js";
5+
import type pino from "pino";
6+
import { type FastifyBaseLogger } from "fastify";
57

68
export type GetFromCacheInput = {
79
redisClient: RedisModule.default;
810
key: string;
911
encryptionSecret?: string;
12+
logger: pino.Logger | FastifyBaseLogger;
1013
};
1114

1215
export type SetInCacheInput = {
@@ -26,6 +29,7 @@ export async function getKey<T extends object>({
2629
redisClient,
2730
key,
2831
encryptionSecret,
32+
logger,
2933
}: GetFromCacheInput): Promise<T | null> {
3034
const data = await redisClient.get(key);
3135
if (!data) {
@@ -40,8 +44,25 @@ export async function getKey<T extends object>({
4044
message: "Encrypted data found but no decryption key provided.",
4145
});
4246
}
43-
const decryptedData = decrypt({ cipherText: decoded.data, encryptionSecret });
44-
return JSON.parse(decryptedData) as T;
47+
try {
48+
const decryptedData = decrypt({
49+
cipherText: decoded.data,
50+
encryptionSecret,
51+
});
52+
return JSON.parse(decryptedData) as T;
53+
} catch (e) {
54+
if (
55+
e instanceof DecryptionError &&
56+
e.message === INVALID_DECRYPTION_MESSAGE
57+
) {
58+
logger.info(
59+
`Invalid decryption, deleting old Redis key and continuing...`,
60+
);
61+
await redisClient.del(key);
62+
return null;
63+
}
64+
throw e;
65+
}
4566
}
4667

4768
export async function setKey({

src/api/plugins/auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
232232
const cachedJwksSigningKey = await getKey<{ key: string }>({
233233
redisClient,
234234
key: `jwksKey:${header.kid}`,
235+
logger: request.log,
235236
});
236237
if (cachedJwksSigningKey) {
237238
signingKey = cachedJwksSigningKey.key;
@@ -271,6 +272,7 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
271272
const cachedRoles = await getKey<string[]>({
272273
key: `authCache:${request.username}:roles`,
273274
redisClient,
275+
logger: request.log,
274276
});
275277
if (cachedRoles) {
276278
request.userRoles = new Set(cachedRoles as AppRoles[]);

src/api/routes/iam.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
6161
region: genericConfig.AwsRegion,
6262
credentials,
6363
}),
64+
redisClient: fastify.redisClient,
6465
};
6566
fastify.log.info(
6667
`Assumed Entra role ${roleArns.Entra} to get the Entra token.`,
@@ -73,6 +74,7 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
7374
return {
7475
smClient: fastify.secretsManagerClient,
7576
dynamoClient: fastify.dynamoClient,
77+
redisClient: fastify.redisClient,
7678
};
7779
};
7880
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().patch(
@@ -94,12 +96,13 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
9496
});
9597
}
9698
const userOid = request.tokenPayload.oid;
97-
const entraIdToken = await getEntraIdToken(
98-
await getAuthorizedClients(),
99-
fastify.environmentConfig.AadValidClientId,
100-
undefined,
101-
genericConfig.EntraSecretName,
102-
);
99+
const entraIdToken = await getEntraIdToken({
100+
clients: await getAuthorizedClients(),
101+
clientId: fastify.environmentConfig.AadValidClientId,
102+
secretName: genericConfig.EntraSecretName,
103+
encryptionSecret: fastify.secretConfig.encryption_key,
104+
logger: request.log,
105+
});
103106
await patchUserProfile(
104107
entraIdToken,
105108
request.username,
@@ -213,10 +216,13 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
213216
},
214217
async (request, reply) => {
215218
const emails = request.body.emails;
216-
const entraIdToken = await getEntraIdToken(
217-
await getAuthorizedClients(),
218-
fastify.environmentConfig.AadValidClientId,
219-
);
219+
const entraIdToken = await getEntraIdToken({
220+
clients: await getAuthorizedClients(),
221+
clientId: fastify.environmentConfig.AadValidClientId,
222+
secretName: genericConfig.EntraSecretName,
223+
encryptionSecret: fastify.secretConfig.encryption_key,
224+
logger: request.log,
225+
});
220226
if (!entraIdToken) {
221227
throw new InternalServerError({
222228
message: "Could not get Entra ID token to perform task.",
@@ -306,10 +312,13 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
306312
group: groupId,
307313
});
308314
}
309-
const entraIdToken = await getEntraIdToken(
310-
await getAuthorizedClients(),
311-
fastify.environmentConfig.AadValidClientId,
312-
);
315+
const entraIdToken = await getEntraIdToken({
316+
clients: await getAuthorizedClients(),
317+
clientId: fastify.environmentConfig.AadValidClientId,
318+
secretName: genericConfig.EntraSecretName,
319+
encryptionSecret: fastify.secretConfig.encryption_key,
320+
logger: request.log,
321+
});
313322
const groupMetadataPromise = getGroupMetadata(entraIdToken, groupId);
314323
const addResults = await Promise.allSettled(
315324
request.body.add.map((email) =>
@@ -550,12 +559,13 @@ No action is required from you at this time.
550559
group: groupId,
551560
});
552561
}
553-
const entraIdToken = await getEntraIdToken(
554-
await getAuthorizedClients(),
555-
fastify.environmentConfig.AadValidReadOnlyClientId,
556-
undefined,
557-
genericConfig.EntraReadOnlySecretName,
558-
);
562+
const entraIdToken = await getEntraIdToken({
563+
clients: await getAuthorizedClients(),
564+
clientId: fastify.environmentConfig.AadValidClientId,
565+
secretName: genericConfig.EntraSecretName,
566+
encryptionSecret: fastify.secretConfig.encryption_key,
567+
logger: request.log,
568+
});
559569
const response = await listGroupMembers(entraIdToken, groupId);
560570
reply.status(200).send(response);
561571
},
@@ -572,16 +582,17 @@ No action is required from you at this time.
572582
onRequest: fastify.authorizeFromSchema,
573583
},
574584
async (request, reply) => {
575-
const entraIdToken = await getEntraIdToken(
576-
await getAuthorizedClients(),
577-
fastify.environmentConfig.AadValidClientId,
578-
undefined,
579-
genericConfig.EntraSecretName,
580-
);
585+
const entraIdToken = await getEntraIdToken({
586+
clients: await getAuthorizedClients(),
587+
clientId: fastify.environmentConfig.AadValidClientId,
588+
secretName: genericConfig.EntraSecretName,
589+
encryptionSecret: fastify.secretConfig.encryption_key,
590+
logger: request.log,
591+
});
581592
const { redisClient } = fastify;
582593
const key = `entra_manageable_groups_${fastify.environmentConfig.EntraServicePrincipalId}`;
583594
const redisResponse = await getKey<{ displayName: string; id: string }[]>(
584-
{ redisClient, key },
595+
{ redisClient, key, logger: request.log },
585596
);
586597
if (redisResponse) {
587598
request.log.debug("Got manageable groups from Redis cache.");

src/api/routes/membership.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
5151
region: genericConfig.AwsRegion,
5252
credentials,
5353
}),
54+
redisClient: fastify.redisClient,
5455
};
5556
fastify.log.info(
5657
`Assumed Entra role ${roleArns.Entra} to get the Entra token.`,
@@ -63,6 +64,7 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
6364
return {
6465
smClient: fastify.secretsManagerClient,
6566
dynamoClient: fastify.dynamoClient,
67+
redisClient: fastify.redisClient,
6668
};
6769
};
6870
const limitedRoutes: FastifyPluginAsync = async (fastify) => {
@@ -106,10 +108,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
106108
message: `${netId} is already a paid member!`,
107109
});
108110
}
109-
const entraIdToken = await getEntraIdToken(
110-
await getAuthorizedClients(),
111-
fastify.environmentConfig.AadValidClientId,
112-
);
111+
const entraIdToken = await getEntraIdToken({
112+
clients: await getAuthorizedClients(),
113+
clientId: fastify.environmentConfig.AadValidClientId,
114+
secretName: genericConfig.EntraSecretName,
115+
encryptionSecret: fastify.secretConfig.encryption_key,
116+
logger: request.log,
117+
});
113118
const paidMemberGroup = fastify.environmentConfig.PaidMemberGroupId;
114119
const isAadMember = await checkPaidMembershipFromEntra(
115120
netId,
@@ -224,10 +229,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
224229
.header("X-ACM-Data-Source", "dynamo")
225230
.send({ netId, isPaidMember: true });
226231
}
227-
const entraIdToken = await getEntraIdToken(
228-
await getAuthorizedClients(),
229-
fastify.environmentConfig.AadValidClientId,
230-
);
232+
const entraIdToken = await getEntraIdToken({
233+
clients: await getAuthorizedClients(),
234+
clientId: fastify.environmentConfig.AadValidClientId,
235+
secretName: genericConfig.EntraSecretName,
236+
encryptionSecret: fastify.secretConfig.encryption_key,
237+
logger: request.log,
238+
});
231239
const paidMemberGroup = fastify.environmentConfig.PaidMemberGroupId;
232240
const isAadMember = await checkPaidMembershipFromEntra(
233241
netId,

src/api/sqs/handlers/emailMembershipPassHandler.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,37 @@ import {
44
runEnvironment,
55
SQSHandlerFunction,
66
} from "../index.js";
7-
import { environmentConfig, genericConfig } from "common/config.js";
8-
import { getAuthorizedClients } from "../utils.js";
7+
import {
8+
environmentConfig,
9+
genericConfig,
10+
SecretConfig,
11+
} from "common/config.js";
12+
import { getAuthorizedClients, getSecretConfig } from "../utils.js";
913
import { getEntraIdToken, getUserProfile } from "api/functions/entraId.js";
1014
import { issueAppleWalletMembershipCard } from "api/functions/mobileWallet.js";
1115
import { generateMembershipEmailCommand } from "api/functions/ses.js";
1216
import { SESClient } from "@aws-sdk/client-ses";
17+
import RedisModule from "ioredis";
18+
19+
let secretConfig: SecretConfig;
1320

1421
export const emailMembershipPassHandler: SQSHandlerFunction<
1522
AvailableSQSFunctions.EmailMembershipPass
1623
> = async (payload, metadata, logger) => {
1724
const email = payload.email;
1825
const commonConfig = { region: genericConfig.AwsRegion };
1926
const clients = await getAuthorizedClients(logger, commonConfig);
20-
const entraIdToken = await getEntraIdToken(
21-
clients,
22-
currentEnvironmentConfig.AadValidClientId,
23-
);
27+
if (!secretConfig) {
28+
secretConfig = await getSecretConfig({ logger, commonConfig });
29+
}
30+
const redisClient = new RedisModule.default(secretConfig.redis_url);
31+
const entraIdToken = await getEntraIdToken({
32+
clients: { ...clients, redisClient },
33+
clientId: currentEnvironmentConfig.AadValidClientId,
34+
secretName: genericConfig.EntraSecretName,
35+
encryptionSecret: secretConfig.encryption_key,
36+
logger,
37+
});
2438
const userProfile = await getUserProfile(entraIdToken, email);
2539
const pkpass = await issueAppleWalletMembershipCard(
2640
clients,

0 commit comments

Comments
 (0)