Skip to content

Detect SAML SSO interstitial and give an actionable error#16

Merged
drogers0 merged 4 commits into
drogers0:mainfrom
mxschmitt:sso-detection
Jun 3, 2026
Merged

Detect SAML SSO interstitial and give an actionable error#16
drogers0 merged 4 commits into
drogers0:mainfrom
mxschmitt:sso-detection

Conversation

@mxschmitt

Copy link
Copy Markdown
Contributor

Problem

When a repo belongs to an org that enforces SAML SSO and your browser session is authenticated but not SSO-authorized for that org, GitHub serves a "Sign in to org" interstitial page with HTTP 200 — so uploadToken is absent even though you have full write access.

Today that surfaces as:

uploadToken not found on repo page — do you have write access to <owner>/<repo>?

…which sends the user down the wrong path: they do have write access (often admin), and the real fix is a 10-second SSO re-authorization. An SSO session is server-side state (~24h, granted only by completing the IdP handshake in a browser), so it is not something more cookies can supply.

Change

GetUploadToken now detects the SSO interstitial and points the user at /orgs/<owner>/sso:

Before

Error uploading shot.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to GymPod/realtime-core?

After

Error uploading shot.png: step 0 (get upload token): GymPod enforces SAML SSO and your session is not authorized for it — authorize in a browser at https://github.com/orgs/GymPod/sso (lasts ~24h), then retry. Write access alone is not enough

(The non-SSO "no token" message is unchanged apart from a short SSO hint, as a fallback if detection ever misses due to markup drift.)

Detection is deliberately conservative

It matches signals specific to the interstitial and scoped to the repo's owner:

  • the owner-scoped /orgs/<owner>/sso link, or
  • the <title>Sign in to <owner></title>.

It intentionally does not match the words "SAML"/"single sign-on" appearing anywhere on the page — those live in GitHub's site chrome/help links on virtually every page (and in any repo that's simply about SAML), which would be a ~100% false positive. Verified against real captured HTML: a normal repo page and crewjam/saml both correctly do not match.

Safety

  • Purely additive: the new logic only runs in the existing match == nil (no-token) branch. The happy path and all other behavior are unchanged — the only difference is which error you get when the upload was already going to fail.
  • Owner is regex-quoted; empty owner returns early (no panic, no match).
  • Link and title checks are case-insensitive (GitHub owners are case-insensitive and the page may render a different case than the user typed).

Tests

Adds internal/upload/token_test.go (first tests for the package):

  • TestIsSAMLProtected — true cases + false-positive guards (site chrome, a SAML-about repo, another org's link, substring owner, regex-metachar owner, case-insensitive link, empty owner).
  • TestGetUploadToken — table-driven via a stubbed http.RoundTripper: happy path (token extracted), the SSO error wording, and the generic message.

go test ./... and go vet ./... pass.

mxschmitt added 4 commits June 3, 2026 10:03
When an org enforces SAML SSO and the browser session is authenticated but not
SSO-authorized for that org, GitHub serves a "Sign in to <owner>" interstitial
(HTTP 200) without the uploadToken. Previously this surfaced as
"do you have write access to <owner>/<repo>?", which is misleading — the user
can have full admin/write and still need an active SSO session.

GetUploadToken now detects the interstitial (via the owner-scoped
/orgs/<owner>/sso link or the "Sign in to <owner>" title — NOT a broad
"contains SAML" match, which false-positives on site chrome and on repos that
are merely about SAML) and points the user at /orgs/<owner>/sso, noting the SSO
grant is server-side (~24h) so copying cookies can't fix it. The generic
no-token message also gains an SSO hint.

Adds internal/upload/token_test.go (first tests for the package): table tests
for isSAMLProtected incl. false-positive guards, and error-wording tests for
GetUploadToken via a stubbed transport. go test ./... and go vet pass.
The previous TestGetUploadToken_Success couldn't actually exercise
GetUploadToken (its URL is hardcoded to github.com), so it silently only
re-tested the uploadToken regex while spinning up an unused httptest server +
client — dead code with a misleading name.

Replace the three GetUploadToken_* functions with one table-driven
TestGetUploadToken that drives the real code path via the stubTransport helper,
now including a genuine happy-path (token extracted) case the old test lacked.
Remove the unused serve/httptest scaffolding. TestIsSAMLProtected (the
false-positive guards that are the point of the patch) is unchanged.

Net: 2 focused test funcs, no dead code, broader real coverage. go test ./... + go vet pass.
Drop the "copying additional cookies cannot fix it (the SSO grant is
server-side)" tangent — it's debugging-context noise a normal user doesn't
need. Keep the load-bearing correction ("Write access alone is not enough"),
which is the whole reason the message exists. Trim the generic branch's SSO
hint to a one-line pointer. Test assertion updated to match.
- Make the /orgs/<owner>/sso link check case-insensitive to match the title
  check and GitHub's case-insensitive owners (a user typing a different case
  than the page renders previously relied on the title fallback alone).
- Return early for an empty owner so detection can never match on it.
- Add regression tests: lowercase-owner vs canonical-case link, substring
  owner non-match, and empty-owner safety.
@drogers0

drogers0 commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Thanks @mxschmitt, this is great.

Pulled it down, build/vet/test all green. Threw a few extra cases at it too — hyphenated org names, orgs where the display name isn't the slug, a non-English title — and the /orgs/<owner>/sso link catches all of them even when the title doesn't, so the OR is pulling its weight.

Opened #17 to track the underlying issue. Tagging this as v1.0.1 shortly — thanks again.

@drogers0 drogers0 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed locally — go vet/test all green and the edge cases hold up (details in the comment above). LGTM.

@drogers0 drogers0 merged commit c4f4266 into drogers0:main Jun 3, 2026
1 check passed
mxschmitt added a commit to mxschmitt/gh-image that referenced this pull request Jun 4, 2026
…shooting

- README: add a skills.sh badge and a one-line "also available as an agent
  skill (npx skills add drogers0/gh-image)" pointer under Installation.
- SKILL.md: update the SSO troubleshooting rows to match v1.0.1 (drogers0#16) — the
  tool now emits a dedicated "enforces SAML SSO …" message and the generic
  no-token message carries its own /orgs/<org>/sso hint.

Rebased onto main (post-drogers0#16).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants