Horizon

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.

app/api/webhooks/okta/route.ts
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.

.env.local
OKTA_EVENT_HOOK_SECRET=replace-with-a-long-random-string

Start the app on port 3000.

npm run dev

Start 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-app
Output
HORIZON: Tunnel connected
  URL          https://my-app.hrzn.run (reserved)
  Forwarding   http://localhost:3000
  Request log  https://hrzn.run/dashboard/tunnels/my-app

Your 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

  1. In the Admin Console, open Workflow, then Event Hooks.
  2. Select Create Event Hook.
  3. Enter a Name.
  4. Enter https://my-app.hrzn.run/api/webhooks/okta as the URL.
  5. Set the Authentication field to authorization.
  6. Set the Authentication secret to the same value as OKTA_EVENT_HOOK_SECRET.
  7. Subscribe to the events you want, for example User deactivated.
  8. Select Save & Continue.
  9. 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:

Output
  GET     200  /api/webhooks/okta

Run Preview, or trigger the event you subscribed to. Horizon prints a POST line:

Output
  POST    204  /api/webhooks/okta

Your app terminal prints one line per event, for example:

Output
Received Okta event: user.lifecycle.deactivate

If 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 GET carries your authentication header too. Check that Authentication secret in Okta matches OKTA_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 dev after 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

On this page