okta-delete-relay / src /server.ts
CharlieBoyer's picture
CharlieBoyer HF Staff
Update src/server.ts
6f6f2f9 verified
import express, { Request, Response } from "express";
// Environment variables
const OKTA_AUTH = process.env.OKTA_EVENT_HOOK_AUTH || ""; // e.g., "Bearer my-shared-secret"
const FORWARD_URL = process.env.FORWARD_URL || ""; // external API endpoint you don't own
const FORWARD_AUTH = process.env.FORWARD_AUTH || ""; // optional auth for that API
const PORT = parseInt(process.env.PORT || "7860", 10); // HF Spaces usually route to 7860
// Minimal types for Okta event payloads
type OktaTarget = {
id?: string;
type?: string; // "User"
alternateId?: string; // usually the user's email
};
type OktaEvent = {
uuid?: string;
eventType?: string; // "user.lifecycle.delete"
published?: string; // ISO timestamp
target?: OktaTarget[];
};
type OktaEventHookBody = {
data?: { events?: OktaEvent[] };
};
// A tiny in-memory LRU-ish idempotency cache (swap for Redis/DB in prod)
class IdempotencyCache {
private maxSize: number;
private map = new Map<string, true>();
constructor(maxSize = 2000) { this.maxSize = maxSize; }
has(uuid: string) { return this.map.has(uuid); }
add(uuid: string) {
this.map.set(uuid, true);
if (this.map.size > this.maxSize) {
const firstKey = this.map.keys().next().value as string | undefined;
if (firstKey !== undefined) {
this.map.delete(firstKey);
}
}
}
}
const IDEMPOTENCY = new IdempotencyCache(2000);
const app = express();
app.disable("x-powered-by");
app.use(express.json({ limit: "256kb" }));
/**
* 1) One-time verification (Okta sends GET with x-okta-verification-challenge header)
*/
app.get("/okta/events", (req: Request, res: Response) => {
const challenge = req.header("x-okta-verification-challenge");
if (!challenge) return res.status(400).send("Missing challenge");
return res.json({ verification: challenge });
});
/**
* 2) Event deliveries (POST). Respond fast (204); process in the background.
*/
app.post("/okta/events", async (req: Request, res: Response) => {
if (OKTA_AUTH) {
const auth = req.header("authorization") || "";
if (auth !== OKTA_AUTH) {
return res.status(401).send("Unauthorized");
}
}
const body = (req.body || {}) as OktaEventHookBody;
const events = body.data?.events ?? [];
// Acknowledge immediately so Okta doesn't retry for slowness
res.status(204).end();
// Continue work asynchronously
setImmediate(() => processEvents(events).catch(console.error));
});
async function processEvents(events: OktaEvent[]) {
for (const evt of events) {
const uuid = evt.uuid;
if (!uuid) continue;
if (IDEMPOTENCY.has(uuid)) continue;
IDEMPOTENCY.add(uuid);
if (evt.eventType !== "user.lifecycle.delete") continue;
const user = (evt.target || []).find(t => t.type === "User");
const email = user?.alternateId;
if (!email) continue;
// Construct the payload expected by the external API (adjust as needed)
const payload = {
email,
eventTime: evt.published,
oktaUserId: user?.id,
eventId: uuid,
};
if (!FORWARD_URL) {
console.warn("FORWARD_URL not set; skipping forward:", payload);
continue;
}
try {
const headers: Record<string, string> = { "content-type": "application/json" };
if (FORWARD_AUTH) headers["authorization"] = FORWARD_AUTH;
const resp = await fetch(FORWARD_URL, {
method: "POST",
headers,
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error(`Forward failed: ${resp.status} ${resp.statusText}`);
}
} catch (err) {
console.error("Forward failed:", err);
// In production, enqueue for retry (e.g., SQS/Queue) rather than dropping it.
}
}
}
app.get("/", (_req, res) => {
res.type("text/plain").send("Okta Event Hook TS relay is running.");
});
app.listen(PORT, () => {
console.log(`Listening on http://0.0.0.0:${PORT}`);
});