eleven / messenger Public

broker: a state-less callback arrival gets an "all set" page, not "bad state" (#533)

Hit live: installing the GitHub App on a second account directly on github.com
bounces the member to the registered callback with a code but no state (our
flow never started), and the broker answered with a bare "bad state" error —
alarming, though the install itself succeeded. A human at that URL with no
state is almost always exactly this, so say so: a small friendly page,
CSP-locked, storing nothing. Real flows (state present) are untouched, and an
unregistered origin still refuses.

Also worth doing at the source: the GitHub App's "Request user authorization
(OAuth) during installation" setting causes these bounces and our plain-
authorize flow doesn't use it — unchecking it stops them entirely.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 6bff8c5e753236ffaecbd2467a9a6415733cae10 parent 611f553
2 files changed, +32 −1
  • broker/broker_test.go +20 −0
  • broker/main.go +12 −1
diff --git a/broker/broker_test.go b/broker/broker_test.go
index 45064e6..a31443c 100644
--- a/broker/broker_test.go
+++ b/broker/broker_test.go
@@ -347,3 +347,23 @@ func TestRegistry(t *testing.T) {
t.Fatalf("registry = schema %d, %d services (want 1, 4)", out.Schema, len(out.Services))
}
}
+
+// A state-less arrival at the callback is a human GitHub bounced here after an
+// on-github.com app install ("request OAuth during installation") — they get a
+// humane "all set" page, never a bare "bad state" error.
+func TestCallbackWithoutStateIsFriendly(t *testing.T) {
+ ts := newTestBroker(t, "http://unused")
+ resp, err := http.Get(ts.URL + "/oauth/github/callback?code=XYZ&setup_action=install&installation_id=1")
+ if err != nil {
+ t.Fatalf("callback: %v", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("got %d, want a 200 page", resp.StatusCode)
+ }
+ body := make([]byte, 512)
+ n, _ := resp.Body.Read(body)
+ if !strings.Contains(string(body[:n]), "All set") {
+ t.Fatalf("page: %q", body[:n])
+ }
+}
diff --git a/broker/main.go b/broker/main.go
index 4c48799..c81d50c 100644
--- a/broker/main.go
+++ b/broker/main.go
@@ -268,7 +268,18 @@ func (s *Server) callback(w http.ResponseWriter, r *http.Request) {
state := q.Get("state")
dot := strings.IndexByte(state, '.')
if dot <= 0 {
- http.Error(w, "bad state", http.StatusBadRequest)
+ // No (or malformed) state usually means GitHub sent someone here after
+ // an app INSTALL made on github.com itself ("request OAuth during
+ // installation" bounces to the registered callback with a code but no
+ // state, since our flow never started). The install worked; a human is
+ // looking at this page — say something humane instead of "bad state".
+ w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`<!doctype html><meta charset="utf-8"><title>All set</title>` +
+ `<body style="font:16px/1.5 system-ui;max-width:34em;margin:4rem auto;color:#333">` +
+ `<h2>All set ✓</h2><p>If you were installing or updating an app on the provider's site, ` +
+ `that worked. You can close this tab and head back to your chat — reconnect or refresh there if needed.</p>`))
return
}
originBytes, err := base64.RawURLEncoding.DecodeString(state[:dot])