Test Okta event hooks locally
Receive Okta Event Hook requests on localhost with a Horizon tunnel, answer the one-time verification GET, and check the Authorization header.
Receive Okta events on your laptop while you build, with a URL Okta can reach.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - An Okta org where you can open the Admin Console
Start your app
Okta does not sign event hook payloads. It sends a secret string of your choice in a header instead, and your service checks it. You pick the header name and the secret when you register the hook. This guide uses the header authorization.
Okta also makes a one-time GET request to verify that you own the endpoint. The request carries an x-okta-verification-challenge header. Return its value in a JSON object named verification. Every later event arrives as a POST with the events in a data.events array.
import { timingSafeEqual } from "node:crypto";
function hasValidSecret(request: Request) {
const secret = process.env.OKTA_EVENT_HOOK_SECRET ?? "";
const received = request.headers.get("authorization") ?? "";
const receivedBuffer = Buffer.from(received);
const secretBuffer = Buffer.from(secret);
return (
secret !== "" &&
receivedBuffer.length === secretBuffer.length &&
timingSafeEqual(receivedBuffer, secretBuffer)
);
}
export async function GET(request: Request) {
if (!hasValidSecret(request)) {
return new Response("Unauthorized", { status: 401 });
}
const challenge = request.headers.get("x-okta-verification-challenge");
return Response.json({ verification: challenge });
}
export async function POST(request: Request) {
if (!hasValidSecret(request)) {
return new Response("Unauthorized", { status: 401 });
}
const payload = await request.json();
for (const event of payload.data.events) {
console.log(`Received Okta event: ${event.eventType}`);
}
return new Response(null, { status: 204 });
}Okta waits 3 seconds for an answer and retries at most once. It retries 5xx responses, not 4xx ones. Return 200 or 204 quickly.
Pick a secret and store it in an environment variable. Use a random, high-entropy string.
OKTA_EVENT_HOOK_SECRET=replace-with-a-long-random-stringStart the app on port 3000.
npm run devStart a tunnel
Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so your Okta event hook would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.
hrzn tunnel http://localhost:3000 -s my-appHORIZON: Tunnel connected
URL https://my-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-appYour public URL is https://my-app.hrzn.run. Keep this terminal open and keep the app running. Okta verifies the endpoint in the next step.
Add the event hook in Okta
- In the Admin Console, open Workflow, then Event Hooks.
- Select Create Event Hook.
- Enter a Name.
- Enter
https://my-app.hrzn.run/api/webhooks/oktaas the URL. - Set the Authentication field to
authorization. - Set the Authentication secret to the same value as
OKTA_EVENT_HOOK_SECRET. - Subscribe to the events you want, for example User deactivated.
- Select Save & Continue.
- Select Verify.
Okta sends the verification GET now. When it succeeds, the hook's status shows VERIFIED.
Preview an event
Okta lets you test a verified hook with sample or historical event data. Open the hook's Actions menu and select Preview.
Check it works
After you select Verify, Horizon prints one line for the verification request:
GET 200 /api/webhooks/oktaRun Preview, or trigger the event you subscribed to. Horizon prints a POST line:
POST 204 /api/webhooks/oktaYour app terminal prints one line per event, for example:
Received Okta event: user.lifecycle.deactivateIf a line shows [401], see Troubleshooting.
Troubleshooting
Verification fails
- Check that the app and the tunnel are both running before you select Verify.
- Check that the URL ends with
/api/webhooks/okta. - The verification
GETcarries your authentication header too. Check that Authentication secret in Okta matchesOKTA_EVENT_HOOK_SECRET, with no extra spaces or newline. - Check that your handler returns JSON of the form
{ "verification": "<value of the challenge header>" }.
The handler returns 401 for events
- Check that Authentication field is
authorization. The handler reads that header. - Restart
npm run devafter you edit.env.local. - Okta doesn't retry 4xx responses, so fix the cause, then trigger a new event.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the URL stays the same. -s needs a subdomain you reserved, see Pricing.
Next steps
- Read Okta's event hooks concepts.
- Follow Okta's event hook implementation guide.
Test Clerk webhooks locally
Receive Clerk webhook events on localhost with a Horizon tunnel, and verify them with verifyWebhook in a Next.js route.
Test Signal Sciences webhooks locally
Receive Fastly Next-Gen WAF (Signal Sciences) webhook notifications on localhost with a Horizon tunnel, and verify X-SigSci-Signature.