Skip to content

feat(e2e): add TOTP support for 2FA-enabled GitHub accounts - #1211

Merged
ggallen merged 8 commits into
mainfrom
ci/e2e-totp-support
May 20, 2026
Merged

feat(e2e): add TOTP support for 2FA-enabled GitHub accounts#1211
ggallen merged 8 commits into
mainfrom
ci/e2e-totp-support

Conversation

@ralphbean

@ralphbean ralphbean commented May 19, 2026

Copy link
Copy Markdown
Member

Summary

  • Add TOTP/2FA support to e2e test infrastructure so the test account can have 2FA enabled
  • New e2e/internal/otp package for shared TOTP code generation (wraps pquerna/otp)
  • handleTOTPIfPresent() handles TOTP forms on both sudo and 2FA pages via Playwright
  • export-session detects and completes 2FA after login (PressSequentially triggers GitHub's auto-submit)
  • E2E_GITHUB_TOTP_SECRET wired through CI workflow, envConfig, and all call sites
  • handleSelectTargetPage() added to handle GitHub's /installations/select_target redirect during app installation — discovered during 2FA CI testing when GitHub changed the installation flow to require org selection
  • Password-path sudo confirmation now falls back to TOTP if password submission doesn't clear the page
  • Verified working: make e2e-export-session and make e2e-upload-session both succeed with 2FA

Why stored sessions are still needed

GitHub blocks password-based login from Actions runner IPs (Azure datacenter ranges). TOTP doesn't change this — the block happens at the /login form before reaching the 2FA prompt. The stored session bypasses login entirely. TOTP is needed for:

  • make e2e-export-session (local login with 2FA to generate the session)
  • Sudo confirmation during e2e runs (GitHub may present TOTP instead of password on sudo pages for 2FA accounts)

Test plan

  • make e2e-export-session succeeds with 2FA
  • make e2e-upload-session succeeds (session uploaded to repo secret)
  • go test ./e2e/internal/otp/ -v — all unit tests pass
  • go vet ./e2e/... — clean
  • e2e package compiles with -tags e2e
  • CI e2e run passes with new E2E_GITHUB_TOTP_SECRET secret (secret already set in repo)

🤖 Generated with Claude Code


Blocked by #1253

@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown

Site preview

Preview: https://d9bf420a-site.fullsend-ai.workers.dev

Commit: 2b16288e9d2c1f7d13856af62ee43ed6242907ce

@fullsend-ai-review

fullsend-ai-review Bot commented May 19, 2026

Copy link
Copy Markdown

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — Protected infrastructure file modified without a linked issue. The workflow change (adding E2E_GITHUB_TOTP_SECRET secret passthrough) is straightforward and well-explained in the PR description, but protected-path changes require a linked issue for traceability. Human approval is required regardless.
    Remediation: Create a tracking issue for the TOTP e2e support work and link it to this PR.

Low

  • [style] e2e/admin/login.gohandleSudoIfPresent grew from ~30 to ~70 lines with five distinct branches and nested fallback logic (password → TOTP fallback, TOTP-only, neither). The control flow is correct but would be clearer if the auth-method selection were extracted into a helper (e.g., trySudoAuth) that returns early on success, keeping handleSudoIfPresent focused on page detection and post-auth verification.
Previous run

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected path requiring human approval. The change adds E2E_GITHUB_TOTP_SECRET to the e2e workflow environment. The PR has no linked issue providing authorization for governance/infrastructure changes. Human review and approval is required for all protected-path modifications regardless of scope.

Low

  • [intent-alignment] e2e/admin/browser.go — The handleSelectTargetPage() function and the /installations/select_target case in the URL dispatcher (lines 59–60 in the diff) are not mentioned in the PR title, description, or ADR. This appears to be a drive-by fix for org selection during GitHub App installation, possibly discovered during 2FA testing. While the change is small and reasonable, it should be documented in the PR description for traceability.

  • [correctness] e2e/admin/login.go — In handleSudoIfPresent, when both password and totpSecret are provided and the password field is visible, the function always takes the password path with no fallback to TOTP if password submission fails (the confirm button click succeeds but the page remains on the sudo page). If GitHub ever presents a password field but requires TOTP for a 2FA account, this path will error without attempting TOTP. Consider adding a TOTP fallback after password submission failure, or document this as a known limitation.

Previous run (2)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected path requiring human approval. The PR has no linked issue providing authorization for this change. While the PR description explains the rationale (wiring E2E_GITHUB_TOTP_SECRET into CI), a linked issue is required for protected-path modifications.
    Remediation: Link an issue authorizing the CI workflow change, or obtain explicit human approval for the .github/workflows/e2e.yml modification.
Previous run (3)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected path requiring human approval. The PR has no linked issue providing authorization for this change. While the PR description explains the rationale (wiring E2E_GITHUB_TOTP_SECRET into CI), a linked issue is required for protected-path modifications.
    Remediation: Link an issue authorizing the CI workflow change, or obtain explicit human approval for the .github/workflows/e2e.yml modification.

Low

  • [correctness] e2e/internal/otp/otp.go:15 — The GenerateCode doc comment says "within 5 seconds of a 30-second period boundary" but the guard rem >= 26 triggers at 4 seconds before the boundary (seconds 26–29), not 5. Minor inaccuracy.
    Remediation: Change the comment to say "within 4 seconds" or adjust the guard to rem >= 25 to match the comment.
Previous run (4)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected path requiring human approval. The PR has no linked issue to authorize the change. The modification itself is minimal (adding E2E_GITHUB_TOTP_SECRET from secrets to the e2e test step environment), but human review is required for all protected-path changes.
    Remediation: A human reviewer must approve this change. Consider linking an issue to document the authorization for modifying CI workflow files.

Low

  • [docs-currency] docs/ADRs/0010-stored-session-for-e2e-browser-auth.md:90 — ADR 0010 describes handleSudoIfPresent() as entering "the password automatically" but the function now also handles TOTP codes. The cross-reference to ADR 0039 partially mitigates this, but the inline description is stale.
    Remediation: Update the description at line 90-91 to mention that handleSudoIfPresent() now handles both password and TOTP sudo prompts.

  • [docs-currency] docs/ADRs/0010-stored-session-for-e2e-browser-auth.md:134 — States "Two repo secrets are required in CI" but with 2FA enabled, three are needed (E2E_GITHUB_SESSION, E2E_GITHUB_PASSWORD, E2E_GITHUB_TOTP_SECRET). ADR 0039 documents this correctly, but the original text in ADR 0010 is now incomplete.
    Remediation: Add a note at line 134 that a third secret (E2E_GITHUB_TOTP_SECRET) is required when the test account has 2FA enabled, or reference ADR 0039 inline.

Previous run (5)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — Protected infrastructure file modified without a linked issue. The change adds E2E_GITHUB_TOTP_SECRET secret pass-through to the e2e workflow, which is explained in the PR description but has no linked issue authorizing changes to CI infrastructure. Human approval is required for all .github/ changes.
    Remediation: Link an issue authorizing the CI workflow change, or obtain explicit human approval for the protected-path modification.

Low

  • [correctness] e2e/admin/login.go:63 — In handleSudoIfPresent, when the password field is visible but password is empty (line 63), the function immediately errors even if totpSecret is set. The code comment says "Try password first, fall back to TOTP" but TOTP fallback only applies when the password field is entirely absent, not when it's visible but the credential is unset. If GitHub shows a password field on a sudo page for a 2FA account where only the TOTP secret is configured, this path will fail.
    Remediation: Consider attempting TOTP fallback when passwordVisible && password == "" && totpSecret != "" before erroring.
Previous run (6)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a protected path (.github/) without a linked issue. The change adds E2E_GITHUB_TOTP_SECRET to the e2e workflow environment. While the PR description explains the rationale, protected-path changes require a linked issue for traceability. Human approval is required.
    Remediation: Link a tracking issue to this PR that authorizes the workflow change, or have a maintainer explicitly approve.

Medium

  • [documentation-currency] CLAUDE.md — The "Running e2e tests" section documents E2E_GITHUB_PASSWORD, E2E_GITHUB_PASSWORD_FILE, and E2E_GITHUB_SESSION_FILE but does not mention the new E2E_GITHUB_TOTP_SECRET environment variable. Contributors with 2FA-enabled test accounts will not discover the option.
    Remediation: Add E2E_GITHUB_TOTP_SECRET to the environment variable list in the "Running e2e tests" section of CLAUDE.md, with a note that it is optional and only needed for 2FA-enabled accounts.

  • [documentation-currency] docs/ADRs/0010-stored-session-for-e2e-browser-auth.md — Several sections are now stale: (1) Line 88 describes handleSudoIfPresent() as entering "the password automatically" — it now also handles TOTP. (2) Lines 130-134 state "Two repo secrets are required in CI" — there are now three with E2E_GITHUB_TOTP_SECRET. (3) Lines 138-139 note "If the test account enables 2FA, the session export must happen after the 2FA step" without noting that TOTP is now automated by the export-session tool.
    Remediation: Update ADR 0010's Consequences section to reflect the third secret and the new TOTP automation in both sudo and session export flows.

Low

  • [correctness] e2e/admin/login.go — In handleSudoIfPresent, when the sudo page shows a visible password field but password is empty and totpSecret is non-empty, the function errors out ("sudo page shows password field but E2E_GITHUB_PASSWORD is not set") instead of falling through to the TOTP path. The comment on line 49 acknowledges GitHub may show "both with a toggle." If GitHub displays both fields on a 2FA account and only totpSecret is configured, this path will fail unnecessarily.
    Remediation: When passwordVisible && password == "" && totpSecret != "", attempt the TOTP path before erroring.
Previous run (7)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a protected path (.github/) without a linked issue. The change adds E2E_GITHUB_TOTP_SECRET to the e2e workflow environment. While the PR description explains the rationale, protected-path changes require a linked issue for traceability. Human approval is required.
    Remediation: Link a tracking issue to this PR that authorizes the workflow change, or have a maintainer explicitly approve.

Medium

  • [documentation-currency] CLAUDE.md — The "Running e2e tests" section documents E2E_GITHUB_PASSWORD, E2E_GITHUB_PASSWORD_FILE, and E2E_GITHUB_SESSION_FILE but does not mention the new E2E_GITHUB_TOTP_SECRET environment variable. Contributors with 2FA-enabled test accounts will not discover the option.
    Remediation: Add E2E_GITHUB_TOTP_SECRET to the environment variable list in CLAUDE.md's e2e section.

  • [documentation-currency] docs/ADRs/0010-stored-session-for-e2e-browser-auth.md — Several sections are now stale: (1) Line 88 describes handleSudoIfPresent() as entering "the password automatically" — it now also handles TOTP. (2) Lines 130-134 state "Two repo secrets are required in CI" — there are now three with E2E_GITHUB_TOTP_SECRET. (3) Lines 138-139 note "If the test account enables 2FA, the session export must happen after the 2FA step" without noting that TOTP is now automated by the export-session tool.
    Remediation: Update ADR 0010's Consequences section to reflect the third secret and the new TOTP automation in both sudo and session export flows.

Low

  • [correctness] e2e/admin/login.go:56 — When the sudo page shows a visible password field but password is empty and totpSecret is non-empty, the function errors out ("sudo page shows password field but E2E_GITHUB_PASSWORD is not set") instead of falling through to TOTP. The comment on line 49 acknowledges GitHub may show "both with a toggle." If GitHub displays both on a 2FA account and only totpSecret is configured, this path will fail.
    Remediation: When passwordVisible && password == "" && totpSecret != "", attempt the TOTP path before erroring.

  • [correctness] e2e/cmd/export-session/main.go:97-132 — The TOTP handling logic (locator lookup, PressSequentially with 50ms delay, navigation wait) is duplicated from handleTOTPIfPresent in login.go. The otp package was created for code sharing, but the browser automation around it is copy-pasted. If the GitHub TOTP form changes (e.g., different element ID), both sites need updating.
    Remediation: Consider extracting the shared TOTP browser automation into a package importable by both export-session (no build tag) and admin (with e2e build tag), or add a comment cross-referencing the two sites.

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread .github/workflows/e2e.yml
Comment thread e2e/admin/login.go
Comment thread e2e/cmd/export-session/main.go

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread .github/workflows/e2e.yml
Comment thread e2e/admin/login.go

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review Squad Report — 10 agents (3x claude-coder, 3x claude-researcher, 2x gemini-code-review, 2x cursor-code-review)

Well-architected PR with clean code, good security practices (TOTP secret never logged, masked in CI, internal/ package), and solid documentation (ADR 0039 properly cross-linked). Two HIGH findings need attention before merge; five MEDIUM findings are worth addressing.

Findings: 2 HIGH, 5 MEDIUM (after dedup from 80+ raw findings, 4 false positives removed)

Severity Finding Consensus
HIGH TOTP retry reuses same code ~93% of the time 8/10 agents
HIGH Sudo TOTP fallback unreachable when password field visible but empty 7/10 agents
MEDIUM TOTP sudo path skips page departure verification 2/10 agents
MEDIUM NetworkIdle wait is fragile (Playwright discourages it) 7/10 agents
MEDIUM GenerateCode TOCTOU gap between boundary sleep and code generation 5/10 agents
MEDIUM No unit tests for EnterTOTPCode (most complex function) 6/10 agents
MEDIUM waitForPageToLeave busy-polls instead of Playwright waits 6/10 agents

See inline comments for details and suggested fixes.

Comment thread e2e/internal/otp/page.go Outdated
Comment thread e2e/admin/login.go
Comment thread e2e/admin/login.go
Comment thread e2e/internal/otp/page.go Outdated
Comment thread e2e/internal/otp/otp.go Outdated
Comment thread e2e/internal/otp/page.go
Comment thread e2e/admin/login.go Outdated
@ggallen

ggallen commented May 20, 2026

Copy link
Copy Markdown
Member

Addressed review findings from the latest review comment:

High (protected-path): Human-approved — this is the PR author authorizing the workflow change.

Low (docs-currency, ADR 0010:90): Updated handleSudoIfPresent() description to mention TOTP handling and cross-reference ADR 0039.

Low (docs-currency, ADR 0010:134): Updated "Two repo secrets" to mention the optional third secret (E2E_GITHUB_TOTP_SECRET) with a cross-reference to ADR 0039. Also updated the stale 2FA line at 140-141.

E2E fix: The sudo confirmation page does not auto-submit TOTP codes like the 2FA login page. Added explicit submit button click after typing the TOTP code.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All 7 findings from the 10-agent review squad have been addressed:

Fixed (6/7):

  • HIGH TOTP retry now waits for next period boundary instead of fixed 2s sleep
  • HIGH Sudo handler now falls back to TOTP when password field is visible but empty
  • MEDIUM Both TOTP paths now call waitForPageToLeave for departure verification
  • MEDIUM NetworkIdle replaced with DOMContentLoaded
  • MEDIUM GenerateCode captures time.Now() once to eliminate TOCTOU gap
  • MEDIUM waitForPageToLeave rewritten with page.WaitForFunction (event-driven)

Acknowledged (1/7):

  • MEDIUM EnterTOTPCode unit tests deferred — reasonable given the mocking cost for e2e support code validated by integration flow

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread .github/workflows/e2e.yml
Comment thread e2e/internal/otp/otp.go

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

ralphbean and others added 7 commits May 20, 2026 11:57
The e2e test account now has 2FA enabled. Add TOTP code generation
so that both export-session (local login) and the e2e test suite
(sudo confirmation) can handle TOTP challenges automatically.

- Add e2e/internal/otp package wrapping pquerna/otp for shared TOTP
  code generation with unit tests
- Add handleTOTPIfPresent() in login.go for Playwright TOTP form
  interaction, used by both sudo handling and export-session
- Update handleSudoIfPresent() to try TOTP when password field is
  not visible (2FA accounts show TOTP on sudo pages)
- Update export-session to detect and complete 2FA after login
- Pass E2E_GITHUB_TOTP_SECRET through CI workflow, envConfig, and
  all call sites

Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
- Verify TOTP submission succeeded by checking URL/title after
  navigation (handleTOTPIfPresent returned true without confirming
  the page actually left the TOTP screen)
- Add debug screenshots on TOTP failure paths (screenshotDir param
  was accepted but unused)
- Fix WaitForURL glob in export-session (exact match could fail if
  GitHub redirects to /dashboard instead of /)
- Fix flaky TestGenerateCodeDeterministic (sleep could cross a 30s
  TOTP window boundary; now handles the boundary case)
- Improve error message when password field is visible but
  E2E_GITHUB_PASSWORD is not set
- Fix stale comment ("Use Type" -> "Use PressSequentially")

Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
- Extract TOTP page interaction into otp.EnterTOTPCode shared helper,
  eliminating divergent duplicate logic between login.go and
  export-session/main.go
- Add period-boundary avoidance in GenerateCode to prevent generating
  codes that expire during the ~300ms typing delay
- Add single retry on TOTP rejection to handle clock skew between CI
  runners and GitHub servers
- Fix waitForPageToLeave to poll page title instead of relying on stale
  DOMContentLoaded state, and propagate Title() errors instead of
  treating them as success

Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
Add the new E2E_GITHUB_TOTP_SECRET env var to the "Running e2e tests"
section of CLAUDE.md so contributors with 2FA-enabled test accounts can
discover it.

Write ADR 0039 documenting the TOTP automation decision and link it from
ADR 0010.

Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
- TOTP fallback when password field visible but empty: try TOTP
  via toggle before erroring
- TOTP retry waits for next 30s period boundary instead of fixed 2s
- Add waitForPageToLeave after TOTP succeeds on sudo page
- Replace NetworkIdle with DOMContentLoaded in EnterTOTPCode
- Fix TOCTOU gap: capture time.Now() once after boundary sleep
- Replace busy-poll waitForPageToLeave with WaitForFunction

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sudo confirmation page does not auto-submit TOTP codes like the
2FA login page does. After typing the code, explicitly click the
submit button if one is visible.

Also updates ADR 0010 to reflect that handleSudoIfPresent now handles
TOTP and that a third secret (E2E_GITHUB_TOTP_SECRET) is needed when
the test account has 2FA enabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GitHub now redirects /installations/new to /installations/select_target
where the user picks a target org. Add handleSelectTargetPage to click
the test org and proceed to the install page.

Also reverts the GenerateCode doc comment back to "4 seconds" — the
guard rem >= 26 covers seconds 26-29 (4-second detection window).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread .github/workflows/e2e.yml
Comment thread e2e/admin/browser.go
Comment thread e2e/admin/login.go
The password path in handleSudoIfPresent now falls back to TOTP if the
password submission doesn't navigate away from the sudo page. Also moves
the confirm-button click into the password branch, removing previously
unreachable dead code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread .github/workflows/e2e.yml
@ggallen

ggallen commented May 20, 2026

Copy link
Copy Markdown
Member

Final fix to be done in #1253.

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.

3 participants