Back to Feed
article 3m read

Adding webhooks turned my one-line disconnect into a teardown with an order

Wiring two issue trackers into my dev-metrics app got the handshake signed and the tokens into a vault, but disconnect now has to delete webhooks before the token that removes them, and the inbound event handlers still trust whoever calls.

Article DepthPractice

Flow

Disconnect order in my app

  1. 1Delete the webhooks

    Needs a working token. If it fails, it is logged and disconnect carries on.

  2. 2Delete the tokens

    Removes both tokens from the vault. A failure here stops the disconnect.

  3. 3Mark the row inactive

    Clears the vault IDs so the next connect creates new tokens.

  4. 4Second tracker

    Skips the webhook step. Not yet checked whether that is safe.

One database update used to be the whole disconnect button. Mark the row inactive, done. That was fine while a tracker connection was just a token. This week connecting started setting things up on the tracker's side too, and one line wasn't enough.

I hooked two issue trackers into my dev-metrics app. That meant a real secrets vault, webhooks, a renewal job and handlers for incoming events. The mistakes were mostly in the boring parts.

A handshake anyone could forge

When you connect, a state value goes out to the tracker and comes back. Mine held the workspace, the user and a time, encoded but not signed. Anyone could fake one.

Now it's signed, it expires after a few minutes, and it must name both the user and the workspace. The callback also checks that the person logged in right now is the person who started. That stops someone from starting a connection with their account and tricking me into finishing it.

The signing code still prints its secret to the log, though. That has to go.

Tokens moved out of the database

The tokens now live in the vault. My database keeps only the vault's IDs for them. Reconnecting overwrites the old tokens in place. If the vault and my database disagree, the first tracker's flow stops and asks the person to disconnect and reconnect. The second tracker's flow tries to recover by reusing a vault ID from an older row for that workspace, and only stops if it finds none.

Why the token has to die last

The first tracker now calls my app when issues change. Removing those webhooks needs a working token. So disconnect goes in this order: delete the webhooks, then delete the tokens, then mark the row inactive and clear the IDs. Flip the first two and the webhooks are stuck pointing at me until they expire.

None of this is all-or-nothing. If webhook cleanup fails, disconnect logs it and still says it worked. The second tracker's disconnect skips webhooks entirely, and I haven't checked whether that's safe.

Webhooks expire

Webhook registration runs in the background after you connect. If it fails, you still see a success screen. The webhooks also expire after about a month, so a daily job renews them. It skips any row missing its webhook info and only writes a log line. A reconnect wipes that info, so I'd expect the renewal to stop for that row.

The inbound side still believes anyone

The first tracker's handler doesn't check who sent an event. It also doesn't know which workspace an event is for, so it picks the first active one. Anyone who finds the URL can write issues there.

The second tracker's handler does check a signature. But the check has a hole when its secret isn't set.

It rejects a request only when a secret is set and the signature doesn't match, so with no secret nothing gets rejected.

The renewal job's trigger has the same shape, which means a missing setting opens it up to anyone.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward