From 6d12f5a342188335b4558407d7c5973322145fea Mon Sep 17 00:00:00 2001 From: qiansc Date: Tue, 27 Oct 2026 07:53:11 +0000 Subject: [PATCH 1/2] fix(sso): start sign-in on the configured site address A login started on the Fly hostname or an alias set the login binding cookie on that host, while the provider returned to the site address, so the callback failed with INVALID_LOGIN_STATE. Send such logins to the sign-in screen on the configured origin first. Co-Authored-By: Claude Opus 5.5 --- docs/authentication.md | 2 +- internal/sso/handler.go | 28 ++++++++++++++++++++ internal/sso/site_login_test.go | 45 +++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 internal/sso/site_login_test.go diff --git a/docs/authentication.md b/docs/authentication.md index 8bb7f50..3a9f6bf 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -36,6 +36,6 @@ Use the Fly dashboard Secrets UI, or `fly secrets import --app context4ai-source ## Sign-in screen -The sign-in screen follows the site language and theme. With no configured provider it explains the missing Google/GitHub configuration and links to this setup guide; configured providers appear as sign-in buttons. A failed provider lookup offers retry without blocking the public browsing link. Login preserves the requested local Source Graph page. +The sign-in screen follows the site language and theme. With no configured provider it explains the missing Google/GitHub configuration and links to this setup guide; configured providers appear as sign-in buttons. A failed provider lookup offers retry without blocking the public browsing link. Login preserves the requested local Source Graph page. Sign-in must start on the configured site address because the provider always returns there and the login cookie belongs to one host. A login started on another address, such as the Fly hostname or an alias reached without the site's proxy, is first sent to the sign-in screen on the site address; the proxy forwards the original host in `X-Forwarded-Host`. Registration denial is a separate, server-rendered HTTP 403 page with the configured bilingual notice, a project link, and a five-second return to the validated previous page. This is distinct from missing OAuth configuration and provider lookup failures. diff --git a/internal/sso/handler.go b/internal/sso/handler.go index 0895d74..a50c4c1 100644 --- a/internal/sso/handler.go +++ b/internal/sso/handler.go @@ -220,7 +220,35 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fail(w, 404, "NOT_FOUND") } } +// siteLogin returns the sign-in page on the configured site address when a login +// starts on another address (the Fly hostname, an alias). The login binding cookie +// is host-only and the provider always returns to the configured address, so a +// login started elsewhere cannot complete. The site's reverse proxy forwards the +// original host; only the configured origin is ever used as a redirect target. +func (h *Handler) siteLogin(r *http.Request) string { + site, err := url.Parse(h.config.Origin) + if err != nil || site.Host == "" { + return "" + } + host := r.Host + if forwarded := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-Host"), ",")[0]); forwarded != "" { + host = forwarded + } + if host == "" || strings.EqualFold(host, site.Host) { + return "" + } + target := safeReturn(r.URL.Query().Get("return_to")) + if target == "" { + target = "/sourcegraph/" + } + return strings.TrimSuffix(h.config.Origin, "/") + "/sourcegraph/login?return_to=" + url.QueryEscape(target) +} + func (h *Handler) login(w http.ResponseWriter, r *http.Request) { + if target := h.siteLogin(r); target != "" { + http.Redirect(w, r, target, http.StatusFound) + return + } if r.Header.Get("Sec-Fetch-Site") == "cross-site" { fail(w, 403, "CROSS_SITE_LOGIN") return diff --git a/internal/sso/site_login_test.go b/internal/sso/site_login_test.go new file mode 100644 index 0000000..3e1e3ae --- /dev/null +++ b/internal/sso/site_login_test.go @@ -0,0 +1,45 @@ +package sso + +import ( + "context" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "github.com/context4ai/sourcegraph/internal/sqlstore" + "golang.org/x/oauth2" +) + +func TestLoginStartsOnTheSiteAddress(t *testing.T) { + db, err := sqlstore.Open(context.Background(), "sqlite", filepath.Join(t.TempDir(), "auth.db")) + if err != nil { + t.Fatal(err) + } + h := &Handler{config: Config{Enabled: true, Origin: "https://context4ai.org"}, store: &SQLStore{DB: db}, Provider: "github", + oauth: oauth2.Config{ClientID: "id", RedirectURL: "https://context4ai.org/sourcegraph/auth/github/callback", Endpoint: oauth2.Endpoint{AuthURL: "https://github.com/login/oauth/authorize"}}} + start := func(host, forwarded string) (int, string, string) { + r := httptest.NewRequest("GET", "http://"+host+"/sourcegraph/auth/login?return_to=%2Fsourcegraph%2Frepos", nil) + r.Host = host + if forwarded != "" { + r.Header.Set("X-Forwarded-Host", forwarded) + } + w := httptest.NewRecorder() + h.login(w, r) + return w.Code, w.Header().Get("Location"), w.Header().Get("Set-Cookie") + } + // The Fly hostname and aliases send the browser to the sign-in page on the site. + for _, host := range [][2]string{{"context4ai-sourcegraph.fly.dev", ""}, {"context4ai-sourcegraph.internal:8080", "www.context4ai.org"}} { + code, location, cookie := start(host[0], host[1]) + if code != 302 || location != "https://context4ai.org/sourcegraph/login?return_to=%2Fsourcegraph%2Frepos" || cookie != "" { + t.Fatalf("%v: %d %s %q", host, code, location, cookie) + } + } + // Through the site's proxy (and on the site itself) the login starts at the provider. + for _, host := range [][2]string{{"context4ai-sourcegraph.internal:8080", "context4ai.org"}, {"context4ai.org", ""}} { + code, location, cookie := start(host[0], host[1]) + if code != 302 || !strings.HasPrefix(location, "https://github.com/login/oauth/authorize") || !strings.Contains(cookie, flowCookie+"=") { + t.Fatalf("%v: %d %s %q", host, code, location, cookie) + } + } +} From 8aa7d46a414b8b6ca1ed405bceaf08c8d67a2f89 Mon Sep 17 00:00:00 2001 From: qiansc Date: Tue, 27 Oct 2026 07:53:22 +0000 Subject: [PATCH 2/2] style: gofmt sso handler Co-Authored-By: Claude Opus 5.5 --- internal/sso/handler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/sso/handler.go b/internal/sso/handler.go index a50c4c1..dcc5f8e 100644 --- a/internal/sso/handler.go +++ b/internal/sso/handler.go @@ -220,6 +220,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fail(w, 404, "NOT_FOUND") } } + // siteLogin returns the sign-in page on the configured site address when a login // starts on another address (the Fly hostname, an alias). The login binding cookie // is host-only and the provider always returns to the configured address, so a