Skip to content

pam/adapter: Ignore stale stopAuthentication from a superseded challenge#1626

Draft
adombeck wants to merge 2 commits into
mainfrom
fix-gdm-challenge-race
Draft

pam/adapter: Ignore stale stopAuthentication from a superseded challenge#1626
adombeck wants to merge 2 commits into
mainfrom
fix-gdm-challenge-race

Conversation

@adombeck

@adombeck adombeck commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

After successful device authentication through GDM, the broker returns "next" and authd switches to the newpassword challenge so the user can set a local password. The "Create a local password" entry would sometimes not appear, leaving the login stuck on the greeter until the device code expired (intermittent; see #1424, #1414).

Switching auth modes tears down the current challenge via authenticationModel.Reset(), which schedules cancelIsAuthenticated() — an asynchronous command that emits stopAuthentication once the in-flight IsAuthenticated call has been cancelled. The new challenge is composed and started in the meantime. When the stale stopAuthentication from the previous challenge's cancellation arrived after the new challenge had started, it cleared inProgress and the entry was never rendered.

Stamp each stopAuthentication with the challenge generation current when it was scheduled, bump the generation when a new challenge is composed, and ignore a stop whose generation no longer matches. A stop belonging to a superseded challenge can no longer tear down the current one.

The exact-event-count assertions added alongside the GDM echo suppression are removed from TestGdmModel: they race with the test conversation handler, which delivers echo and stage-change events concurrently with the model's polls. The invariant they checked (the echo of a selection must not add an extra selection cycle) is covered deterministically in gdmmodel_authmode_echo_test.go.

adombeck and others added 2 commits June 25, 2026 17:09
Device authentication through GDM could hang on the QR screen even after
the user completed the browser flow. The QR screen showed one device code
while the broker kept polling for a different one, so the authorization
the user granted was never observed and the login stalled until the
device code expired.

When an auth mode is auto-selected, the adapter both calls
SelectAuthenticationMode (minting a device code and starting the poll)
and tells GDM to select that mode. GDM echoes the selection back in its
next poll, and the adapter treated that echo as a fresh selection,
issuing a second SelectAuthenticationMode that minted a new device code
and orphaned the first poll.

Track the selection we send to GDM and drop its echo. The suppression is
a one-shot consumed by the first matching echo, and is also cleared on
any stage change, so a genuine re-selection of the same mode (the user
navigating back to auth mode selection and picking it again) is still
honored. A pure stage-change reset raced with the echo in the
change-password flow; a pure one-shot swallowed legitimate re-selection
of an auto-selected single mode. Combining both covers each gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After successful device authentication through GDM, the broker returns
"next" and authd switches to the newpassword challenge so the user can
set a local password. The "Create a local password" entry would
sometimes not appear, leaving the login stuck on the greeter until the
device code expired (intermittent; see #1424, #1414).

Switching auth modes tears down the current challenge via
authenticationModel.Reset(), which schedules cancelIsAuthenticated() —
an asynchronous command that emits stopAuthentication once the in-flight
IsAuthenticated call has been cancelled. The new challenge is composed
and started in the meantime. When the stale stopAuthentication from the
previous challenge's cancellation arrived after the new challenge had
started, it cleared inProgress and the entry was never rendered.

Stamp each stopAuthentication with the challenge generation current when
it was scheduled, bump the generation when a new challenge is composed,
and ignore a stop whose generation no longer matches. A stop belonging
to a superseded challenge can no longer tear down the current one.

The exact-event-count assertions added alongside the GDM echo
suppression are removed from TestGdmModel: they race with the test
conversation handler, which delivers echo and stage-change events
concurrently with the model's polls. The invariant they checked (the
echo of a selection must not add an extra selection cycle) is covered
deterministically in gdmmodel_authmode_echo_test.go.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adombeck adombeck added the e2e-tests This issue is related to end-to-end tests / Run end-to-end tests on this pull request label Jun 25, 2026
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.00%. Comparing base (31f8478) to head (c61f309).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
pam/internal/adapter/authentication.go 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1626      +/-   ##
==========================================
+ Coverage   84.24%   88.00%   +3.75%     
==========================================
  Files          21       99      +78     
  Lines        1168     6843    +5675     
  Branches        0      111     +111     
==========================================
+ Hits          984     6022    +5038     
- Misses        184      765     +581     
- Partials        0       56      +56     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

}

func (m gdmModel) handlePollResponse(gdmPollResults []*gdm.EventData) tea.Cmd {
func (m *gdmModel) handlePollResponse(gdmPollResults []*gdm.EventData) tea.Cmd {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The change here goes against the point of using returned copy of the model itself to do changes, in fact ideally no method should use a pointer receiver.

@adombeck adombeck changed the title pam/gdm: Ignore GDM echo of our own auth mode selection pam/adapter: Ignore stale stopAuthentication from a superseded challenge Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e2e-tests This issue is related to end-to-end tests / Run end-to-end tests on this pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants