Skip to content

Commit b2cf6ac

Browse files
authored
Fix auth caching (#185)
1 parent 2d88a2d commit b2cf6ac

File tree

13 files changed

+104
-12
lines changed

13 files changed

+104
-12
lines changed

src/api/functions/authorization.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { unmarshall } from "@aws-sdk/util-dynamodb";
33
import { genericConfig } from "../../common/config.js";
44
import { DatabaseFetchError } from "../../common/errors/index.js";
55
import { allAppRoles, AppRoles } from "../../common/roles.js";
6+
import type Redis from "ioredis";
7+
import { AUTH_CACHE_PREFIX } from "api/plugins/auth.js";
8+
import type pino from "pino";
9+
import { type FastifyBaseLogger } from "fastify";
610

711
export async function getUserRoles(
812
dynamoClient: DynamoDBClient,
@@ -63,3 +67,27 @@ export async function getGroupRoles(
6367
}
6468
return items.roles as AppRoles[];
6569
}
70+
71+
type ClearAuthCacheInput = {
72+
redisClient: Redis.default;
73+
username: string[];
74+
logger: pino.Logger | FastifyBaseLogger;
75+
};
76+
export async function clearAuthCache({
77+
redisClient,
78+
username,
79+
logger,
80+
}: ClearAuthCacheInput) {
81+
logger.debug(`Clearing auth cache for: ${JSON.stringify(username)}.`);
82+
const keys = (
83+
await Promise.all(
84+
username.map((x) => redisClient.keys(`${AUTH_CACHE_PREFIX}${x}*`)),
85+
)
86+
).flat();
87+
if (keys.length === 0) {
88+
return 0;
89+
}
90+
const result = await redisClient.del(keys);
91+
logger.debug(`Cleared ${result} auth cache keys.`);
92+
return result;
93+
}

src/api/functions/entraId.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ export async function getEntraIdToken({
109109
key: cacheKey,
110110
data: JSON.stringify({ token: result.accessToken }),
111111
expiresIn:
112-
(result.expiresOn.getTime() - new Date().getTime()) / 1000 - 120, // get new token 2 min before expiry
112+
Math.floor(
113+
(result.expiresOn.getTime() - new Date().getTime()) / 1000,
114+
) - 120, // get new token 2 min before expiry
113115
encryptionSecret,
114116
});
115117
}
@@ -118,8 +120,9 @@ export async function getEntraIdToken({
118120
if (error instanceof BaseError) {
119121
throw error;
120122
}
123+
logger.error(error);
121124
throw new InternalServerError({
122-
message: `Failed to acquire token: ${error}`,
125+
message: "Failed to acquire Entra ID token.",
123126
});
124127
}
125128
}

src/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
import { ZodOpenApiVersion } from "zod-openapi";
4949
import { withTags } from "./components/index.js";
5050
import apiKeyRoute from "./routes/apiKey.js";
51+
import clearSessionRoute from "./routes/clearSession.js";
5152
import RedisModule from "ioredis";
5253

5354
dotenv.config();
@@ -311,6 +312,7 @@ async function init(prettyPrint: boolean = false) {
311312
api.register(roomRequestRoutes, { prefix: "/roomRequests" });
312313
api.register(logsPlugin, { prefix: "/logs" });
313314
api.register(apiKeyRoute, { prefix: "/apiKey" });
315+
api.register(clearSessionRoute, { prefix: "/clearSession" });
314316
if (app.runEnvironment === "dev") {
315317
api.register(vendingPlugin, { prefix: "/vending" });
316318
}

src/api/lambda.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const handler = async (event: APIGatewayEvent, context: Context) => {
1717
return "warmed";
1818
}
1919
// else proceed with handler logic
20-
return realHandler(event, context).catch((e) => {
20+
return await realHandler(event, context).catch((e) => {
2121
console.error(e);
2222
const newError = new InternalServerError({
2323
message: "Failed to initialize application.",

src/api/plugins/auth.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { getGroupRoles, getUserRoles } from "../functions/authorization.js";
2222
import { getApiKeyData, getApiKeyParts } from "api/functions/apiKey.js";
2323
import { getKey, setKey } from "api/functions/redisCache.js";
2424

25+
export const AUTH_CACHE_PREFIX = `authCache:`;
26+
2527
export function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
2628
const _intersection = new Set<T>();
2729
for (const elem of setB) {
@@ -270,7 +272,7 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
270272
verifiedTokenData.sub;
271273
const expectedRoles = new Set(validRoles);
272274
const cachedRoles = await getKey<string[]>({
273-
key: `authCache:${request.username}:roles`,
275+
key: `${AUTH_CACHE_PREFIX}${request.username}:roles`,
274276
redisClient,
275277
logger: request.log,
276278
});
@@ -326,7 +328,7 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
326328
}
327329
request.userRoles = userRoles;
328330
setKey({
329-
key: `authCache:${request.username}:roles`,
331+
key: `${AUTH_CACHE_PREFIX}${request.username}:roles`,
330332
data: JSON.stringify([...userRoles]),
331333
redisClient,
332334
expiresIn: GENERIC_CACHE_SECONDS,

src/api/routes/clearSession.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { FastifyPluginAsync } from "fastify";
2+
import rateLimiter from "api/plugins/rateLimiter.js";
3+
import { withRoles, withTags } from "api/components/index.js";
4+
import { clearAuthCache } from "api/functions/authorization.js";
5+
6+
const clearSessionPlugin: FastifyPluginAsync = async (fastify, _options) => {
7+
fastify.register(rateLimiter, {
8+
limit: 10,
9+
duration: 30,
10+
rateLimitIdentifier: "clearSession",
11+
});
12+
fastify.post(
13+
"",
14+
{
15+
schema: withRoles(
16+
[],
17+
withTags(["Generic"], {
18+
summary: "Clear user's session (usually on logout).",
19+
hide: true,
20+
}),
21+
),
22+
onRequest: fastify.authorizeFromSchema,
23+
},
24+
async (request, reply) => {
25+
reply.status(201).send();
26+
const username = [request.username!];
27+
const { redisClient } = fastify;
28+
const { log: logger } = fastify;
29+
await clearAuthCache({ redisClient, username, logger });
30+
},
31+
);
32+
};
33+
34+
export default clearSessionPlugin;

src/api/routes/iam.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
EntraGroupActions,
3333
entraProfilePatchRequest,
3434
} from "../../common/types/iam.js";
35-
import { getGroupRoles } from "../functions/authorization.js";
35+
import { clearAuthCache, getGroupRoles } from "../functions/authorization.js";
3636
import { getRoleCredentials } from "api/functions/sts.js";
3737
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
3838
import { createAuditLogEntry } from "api/functions/auditLog.js";
@@ -162,6 +162,14 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
162162
const groupId = (request.params as Record<string, string>).groupId;
163163
try {
164164
const timestamp = new Date().toISOString();
165+
const entraIdToken = await getEntraIdToken({
166+
clients: await getAuthorizedClients(),
167+
clientId: fastify.environmentConfig.AadValidClientId,
168+
secretName: genericConfig.EntraSecretName,
169+
encryptionSecret: fastify.secretConfig.encryption_key,
170+
logger: request.log,
171+
});
172+
const groupMembers = listGroupMembers(entraIdToken, groupId);
165173
const command = new PutItemCommand({
166174
TableName: `${genericConfig.IAMTablePrefix}-grouproles`,
167175
Item: marshall({
@@ -187,6 +195,13 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
187195
request.body.roles,
188196
GENERIC_CACHE_SECONDS,
189197
);
198+
const groupMemberEmails = (await groupMembers).map((x) => x.email);
199+
await clearAuthCache({
200+
redisClient: fastify.redisClient,
201+
username: groupMemberEmails,
202+
logger: request.log,
203+
});
204+
reply.send({ message: "OK" });
190205
} catch (e: unknown) {
191206
fastify.nodeCache.del(`grouproles-${groupId}`);
192207
if (e instanceof BaseError) {
@@ -198,7 +213,6 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
198213
message: "Could not create group role mapping.",
199214
});
200215
}
201-
reply.send({ message: "OK" });
202216
},
203217
);
204218
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post(
@@ -525,6 +539,14 @@ No action is required from you at this time.
525539
);
526540
}
527541
}
542+
const allEmailsModified = response.success.map((x) => x.email);
543+
const { redisClient } = fastify;
544+
const { log: logger } = request;
545+
await clearAuthCache({
546+
redisClient,
547+
username: allEmailsModified,
548+
logger,
549+
});
528550
await Promise.allSettled(logPromises);
529551
reply.status(202).send(response);
530552
},

src/common/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;
88

99
type AzureRoleMapping = Record<string, readonly AppRoles[]>;
1010

11-
export const GENERIC_CACHE_SECONDS = 300;
11+
export const GENERIC_CACHE_SECONDS = 600;
1212

1313
export type ConfigType = {
1414
UserFacingUrl: string;

src/ui/components/ProfileDropdown/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useState } from "react";
1919
import { AuthContextData, useAuth } from "../AuthContext/index.js";
2020
import classes from "../Navbar/index.module.css";
2121
import { useNavigate } from "react-router-dom";
22+
import { useApi } from "@ui/util/api.js";
2223

2324
interface ProfileDropdownProps {
2425
userData?: AuthContextData;
@@ -31,6 +32,7 @@ const AuthenticatedProfileDropdown: React.FC<ProfileDropdownProps> = ({
3132
const theme = useMantineTheme();
3233
const navigate = useNavigate();
3334
const { logout } = useAuth();
35+
const api = useApi("core");
3436
if (!userData) {
3537
return null;
3638
}
@@ -129,6 +131,7 @@ const AuthenticatedProfileDropdown: React.FC<ProfileDropdownProps> = ({
129131
variant="outline"
130132
fullWidth
131133
onClick={async () => {
134+
await api.post("/api/v1/clearSession");
132135
await logout();
133136
}}
134137
>

tests/unit/auth.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { expect, test, vi } from "vitest";
2-
import { mockClient } from "aws-sdk-client-mock";
32
import init from "../../src/api/index.js";
43
import {
54
secretObject,

0 commit comments

Comments
 (0)