File size: 3,936 Bytes
078bfcc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f6f2f9
 
 
 
078bfcc
6f6f2f9
078bfcc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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}`);
});