Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/github-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ If GitHub sent them back after **Install & Authorize**, Syncidian records the `i
| Symptom | Likely cause |
| --- | --- |
| “This instance has no GitHub App yet” | Finish Option A or B. Confirm `/admin` shows **Registered**, or the `SYNCIDIAN_GITHUB_APP_*` variables are set. |
| OAuth error / redirect mismatch | Callback URL on the GitHub App must be exactly `{base}/api/v1/auth/github/callback`. `{base}` must be the origin in the address bar (scheme + host + port). |
| Redirect URI error after changing the public domain | App ID, Client ID, client secret, and private key stay the same. Edit the **existing** GitHub App: Homepage, Callback, Setup, and Webhook must all use the new `{base}` (for hosted: `https://syncidian.com`). Do not create a new app. |
| OAuth error / redirect mismatch / GitHub **Be careful!** *The `redirect_uri` is not associated with this application* | The GitHub App’s **Callback URL** (Identifying and authorizing users) must be exactly `{base}/api/v1/auth/github/callback`. After moving off a Railway hostname, add `https://syncidian.com/api/v1/auth/github/callback` on the **existing** app and save. Do not create a new app. |
| Redirect URI error after changing the public domain | App ID, Client ID, client secret, and private key stay the same. Edit the **existing** GitHub App: Homepage, Callback, Setup, and Webhook must all use the new `{base}` (for hosted: `https://syncidian.com`). |
| Install succeeds but Syncidian says credentials are missing | Setup URL must be `{base}/api/v1/github/app/setup`. Sign in as a **non-admin** vault user; admins do not connect a repo. |
| Install & Authorize succeeds but dashboard still says **Not configured** | With OAuth during installation, GitHub redirects to the **Callback URL** with `installation_id`. Syncidian must receive that on `/api/v1/auth/github/callback` while you are signed in. Click **Connect with GitHub** again from the dashboard (do not open the install URL in a private window). Confirm Callback URL is exactly `{base}/api/v1/auth/github/callback`. |
| Cannot install on someone else’s GitHub account | Make the app installable on **Any account** (public to GitHub users, not the Marketplace). |
Expand Down
4 changes: 2 additions & 2 deletions internal/server/auth_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (s *Server) handleGitHubAuthStart(w http.ResponseWriter, r *http.Request) {
Secure: r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https"),
MaxAge: 600,
})
urls := githubapp.AppURLs(s.requestBase(r))
urls := githubapp.AppURLs(s.githubBase(r))
q := url.Values{
"client_id": {app.ClientID},
"redirect_uri": {urls.Callback},
Expand Down Expand Up @@ -115,7 +115,7 @@ func (s *Server) handleGitHubAuthCallback(w http.ResponseWriter, r *http.Request
s.dashboardRedirect(w, r, url.Values{"github": {"error"}, "message": {"GitHub App is not registered."}})
return
}
urls := githubapp.AppURLs(s.requestBase(r))
urls := githubapp.AppURLs(s.githubBase(r))
token, err := githubapp.ExchangeOAuth(app.ClientID, app.ClientSecret, r.URL.Query().Get("code"), urls.Callback)
if err != nil {
s.dashboardRedirect(w, r, url.Values{"github": {"error"}, "message": {err.Error()}})
Expand Down
48 changes: 48 additions & 0 deletions internal/server/auth_github_allowlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/shangeethsivan/Syncidian/internal/config"
Expand Down Expand Up @@ -78,6 +80,52 @@ func TestSetupHidesGitHubSignInOnHostedDomain(t *testing.T) {
}
}

func TestGitHubOAuthRedirectUsesPublicURL(t *testing.T) {
dir := t.TempDir()
st, err := store.Open(dir)
if err != nil {
t.Fatal(err)
}
defer st.Close()
srv := New(config.Config{
Addr: ":0", DataDir: dir, PublicURL: "https://syncidian.com",
}, st, nil)
hs := httptest.NewServer(srv.Handler())
defer hs.Close()

adminCookies := setupAdmin(t, hs)
res, m := doJSON(t, http.MethodPost, hs.URL+"/api/v1/github/app/register", map[string]any{
"app_id": 42, "slug": "syncidian", "pem": "-----BEGIN PRIVATE KEY-----\nMIIB\n-----END PRIVATE KEY-----",
"client_id": "Iv1.cb", "client_secret": "secret",
}, adminCookies, "")
if res.StatusCode != 200 {
t.Fatalf("register: %d %v", res.StatusCode, m)
}

req, err := http.NewRequest(http.MethodGet, hs.URL+"/api/v1/auth/github/start", nil)
if err != nil {
t.Fatal(err)
}
req.Host = "something.up.railway.app"
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "something.up.railway.app")
redir, err := noFollowClient().Do(req)
if err != nil {
t.Fatal(err)
}
redir.Body.Close()
if redir.StatusCode != http.StatusFound {
t.Fatalf("start: %d", redir.StatusCode)
}
loc := redir.Header.Get("Location")
if !strings.Contains(loc, "redirect_uri="+url.QueryEscape("https://syncidian.com/api/v1/auth/github/callback")) {
t.Fatalf("OAuth must use PublicURL callback, not the Railway host: %q", loc)
}
if strings.Contains(loc, "railway.app") {
t.Fatalf("OAuth must not send a Railway redirect_uri: %q", loc)
}
}

func TestSetupShowsGitHubSignInOnLocalhost(t *testing.T) {
hs, done := newTestServer(t)
defer done()
Expand Down
11 changes: 5 additions & 6 deletions internal/server/github_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,12 @@ func (s *Server) requestBase(r *http.Request) string {
return proto + "://" + host
}

// githubBase is the origin GitHub can reach (callback, setup, webhook).
// Operator UI on a Tailscale-only hostname must still advertise the public URL.
// githubBase is the origin GitHub can reach (callback, setup, webhook, OAuth).
// Prefer SYNCIDIAN_PUBLIC_URL so sign-in always uses the URL registered on the
// GitHub App (custom domain), not a Tailscale or Railway hostname.
func (s *Server) githubBase(r *http.Request) string {
if s.isAdminHost(r) {
if u := strings.TrimRight(s.Cfg.PublicURL, "/"); u != "" {
return u
}
if u := strings.TrimRight(s.Cfg.PublicURL, "/"); u != "" {
return u
}
return s.requestBase(r)
}
Expand Down
2 changes: 2 additions & 0 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,8 @@ func TestDashboardServed(t *testing.T) {
for _, needle := range []string{
`id="landing"`,
`id="landing-brand"`,
`id="adm-callback-public"`,
`redirect_uri is not associated`,
`github-signin`,
`syncidian-github-signin`,
`Continue with GitHub`,
Expand Down
7 changes: 6 additions & 1 deletion internal/web/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,7 @@ <h1 id="help-title">Setup walkthrough</h1>
if (!urls) return;
const set = (id, v) => { const el = $(id); if (el) el.textContent = v; };
set("adm-callback", urls.callback);
set("adm-callback-public", urls.callback);
set("adm-setup", urls.setup);
set("adm-webhook", urls.webhook);
}
Expand Down Expand Up @@ -1385,7 +1386,11 @@ <h2>GitHub App</h2>
: `<span class="pill warn">Not registered</span> ${waitlistOn()
? "People cannot sign in with GitHub until an operator registers the app."
: "Email login already works. Register the app only if you want GitHub sign-in or per-user backup."}`}</p>
${nerdsOn() ? "" : `<p class="muted">Enable <b>Stats for Nerds</b> for callback URLs, Create GitHub App, and credential paste.</p>`}
${publicInfo && publicInfo.github_app && publicInfo.github_app.configured && publicInfo.github_app.urls && publicInfo.github_app.urls.callback ? `
<p>GitHub sign-in sends people to this <b>Callback URL</b>. It must be listed on the GitHub App (Identifying and authorizing users). If GitHub shows <b>The redirect_uri is not associated with this application</b>, add this exact URL and save — do not leave a Railway hostname as the only callback after you attach a custom domain.</p>
<p><code id="adm-callback-public">${esc(publicInfo.github_app.urls.callback)}</code></p>
<p><a href="https://github.com/settings/apps/${esc(publicInfo.github_app.slug || "syncidian")}" target="_blank" rel="noopener">Open GitHub App settings</a></p>` : ""}
${nerdsOn() ? "" : `<p class="muted">Enable <b>Stats for Nerds</b> for setup/webhook URLs, Create GitHub App, and credential paste.</p>`}
</div>
${nerdsOn() ? `
<div class="card">
Expand Down
Loading