Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: add distributed protection for external auth refresh - #27479

Merged
code-asher merged 14 commits into
mainfrom
asher/external-auth-distributed-race
Aug 19, 2026
Merged

fix: add distributed protection for external auth refresh#27479
code-asher merged 14 commits into
mainfrom
asher/external-auth-distributed-race

Conversation

@code-asher

@code-asher code-asher commented Jul 23, 2026

Copy link
Copy Markdown
Member

Before refreshing, the caller has to put a lease on the row to prevent any other replicas from trying to refresh using the same token. Callers unable to get a lease enter a holding pattern waiting for the results.

Also use this same lease query to re-read the link. This more or less functions the same as the re-read we used to have after a refresh failure, except it also allows us to avoid making the failed request in the first place. The downside is that we have to refetch the link every time even if the caller just fetched it, but since we have to make a database call anyway...

@code-asher code-asher changed the title fix: Add distributed protection for external auth refresh fix: add distributed protection for external auth refresh Jul 24, 2026
@code-asher
code-asher force-pushed the asher/external-auth-distributed-race branch 13 times, most recently from 84a83c7 to 73c7af1 Compare July 27, 2026 19:48
@code-asher
code-asher marked this pull request as ready for review July 27, 2026 20:24
@code-asher
code-asher requested a review from Emyrk July 27, 2026 20:24
@github-actions github-actions Bot added the stale This issue is like stale bread. label Aug 10, 2026
@code-asher
code-asher force-pushed the asher/external-auth-distributed-race branch from 73c7af1 to b499ccc Compare August 10, 2026 17:42
@code-asher code-asher removed the stale This issue is like stale bread. label Aug 10, 2026
@Emyrk

Emyrk commented Aug 10, 2026

Copy link
Copy Markdown
Member

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-08-10 20:36 UTC by @Emyrk

Review history
  • R1 (2026-08-10), 3 Nit, 1 Note, 1 P2, 1 P3, COMMENT. Review

deep-review v0.9.0 | Round 1 | 2f34e1a..b499ccc

Last posted: Round 1, 6 findings (1 P2, 1 P3, 3 Nit, 1 Note), COMMENT. Review

Finding inventory

Finding inventory: PR 27479

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Open coderd/externalauth/externalauth.go:301 Early return on refresh-token change skips cached-failure check and token validation; waiter reports failed refresh as success R1 Netero Yes
CRF-2 P3 Open coderd/externalauth/externalauth.go:275 sql.ErrNoRows from lease acquire conflates "lease held" with "row deleted"; deleted link polls until timeout and surfaces a 500 R1 Netero Yes
CRF-3 Nit Open coderd/externalauth/externalauth.go:61 Doc comment on defaultRefreshLeaseMaxBackoff names the wrong constant R1 Netero Yes
CRF-4 Nit Open coderd/externalauth/externalauth.go:169 Doc comment on RefreshLeaseMaxBackoff names the wrong field R1 Netero Yes
CRF-5 Nit Open coderd/externalauth/externalauth_test.go:1175 require.Equal arguments swapped (actual first, expected second) R1 Netero Yes
CRF-6 Note Open coderd/externalauth/externalauth.go:270 Providers with ValidateURL now acquire and release a lease (two DB writes) on every token fetch even when no refresh is needed R1 Netero Yes

Round log

Round 1

Netero-only (P2 present, panel deferred). 1 P2, 1 P3, 3 Nit, 1 Note. Reviewed against 2f34e1a..b499ccc.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

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.

This is a first-pass review only: these are mechanical findings from Netero, the full review panel has not yet reviewed this PR. The panel will review after these findings are addressed.

The lease design is solid: the acquire/release queries have accurate conditional-update semantics, the deferred errors.Join on release surfaces release failures (and is tested), all new code uses dbtime.Now(), and the removal of UpdateExternalAuthLinkRefreshToken left no stale references across dbauthz, dbcrypt, dbmock, and dbmetrics. Test density is 64.6% with real DB-state assertions.

Severity count: 1 P2, 1 P3, 3 Nit, 1 Note.

A quote from the first pass: "The failure the winner recorded is silently converted into a false success for every waiting replica."

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/externalauth/externalauth.go Outdated
Comment thread coderd/externalauth/externalauth.go Outdated
Comment thread coderd/externalauth/externalauth.go Outdated
Comment thread coderd/externalauth/externalauth.go Outdated
Comment thread coderd/externalauth/externalauth_test.go Outdated
Comment thread coderd/externalauth/externalauth.go Outdated
Comment thread coderd/externalauth/externalauth.go Outdated
Before refreshing, the caller has to put a lease on the row to
prevent any other replicas from trying to refresh using the same token.
Callers unable to get a lease enter a holding pattern waiting for the
results.

Also use this same lease query to re-read the link.  This more or less
functions the same as the re-read we used to have after a refresh
failure, except it also allows us to avoid making the failed request in
the first place.  The downside is that we have to refetch the link every
time even if the caller just fetched it, but since we have to make a
database call anyway...
@code-asher
code-asher force-pushed the asher/external-auth-distributed-race branch 3 times, most recently from b174d96 to 7741c7e Compare August 17, 2026 21:36
If we have to refresh, we do still validate while holding the lease,
since we have it anyway.

But if we do not need to refresh, validate without getting a lease.  The
assumption is that the overhead to get the lease is greater than the
overhead of concurrent validations.
Previously if the token changed (including getting removed) we just
returned it, but it could have errored.
This lets us distinguish between the row not existing and not being able
to take the lease because something else has it.
@code-asher
code-asher force-pushed the asher/external-auth-distributed-race branch from 7741c7e to b59ef04 Compare August 17, 2026 21:51
@code-asher

Copy link
Copy Markdown
Member Author

@jeremyruppel to give an overview of the work and changes so far:

  1. Add a lease column on the link rows, this is used to indicate that another replica is refreshing the token and other replicas will wait for that to complete when they see that lease. This is to avoid having to hold a lock during the whole refresh since that consumes the limited DB connections (one per replica).
  2. However we do still need a lock to update the lease column on the row, but it is limited to just updating that row so it is short-lived. Initially I tried only updating the column if the lease is not already taken via the WHERE clause to avoid the lock entirely but that makes it impossible to distinguish between the row having been deleted and something else having taken the lease (both cases return ErrNoRows).
  3. I had to refactor the validation a little to break it apart from the refresh. So now when we need to refresh, we take the lease and do both refresh and validate while under that lease. When we do not need to refresh, we just validate without taking the lease, under the assumption validate alone is cheaper than the leasing overhead and validate does not require the same concurrency protections that refresh does.
  4. I refactored the mock DB setup to make it easier to assert the whole lock and lease dance since so many of the tests need that.
  5. Due to this new pattern, we no longer need that separate query (UpdateExternalAuthLinkRefreshToken) to update the refresh token with the optimistic lock, since we have a "proper" lock now.

Comment thread coderd/externalauth/externalauth.go Outdated
@jeremyruppel

Copy link
Copy Markdown
Contributor

@code-asher I discussed this with an agent and we found two race conditiony things. I'm going to have the agent post the comments here, but I did discuss these at length so I'm really using this to pass on the excruciating details

Comment thread coderd/externalauth/externalauth.go Outdated
Comment thread coderd/externalauth/externalauth.go Outdated
@code-asher

Copy link
Copy Markdown
Member Author

I fixed the clock drift issue by moving the lease computation into the query and and I added the extra lease guard for the release step just in case.

While I was refactoring those queries, I went in a bit of a different direction with the lock. The reason for the lock is to differentiate between something already having the lease and the row no longer existing, so I created a sql function instead to do that work. No need for the lock anymore.

Postgres's default isolation level ensures that two UPDATEs will not race (even if two queries ran at the same time, once one of them commits a row change, the WHERE clause of the other is re-evaluated, so it would not match the second time around). Added a test to ensure that is the case as well.

Comment thread coderd/database/migrations/000574_external_auth_lock.up.sql Outdated
Comment thread coderd/externalauth/externalauth_test.go
@code-asher
code-asher force-pushed the asher/external-auth-distributed-race branch 2 times, most recently from d312290 to 4c9fe79 Compare August 19, 2026 20:25
@code-asher
code-asher force-pushed the asher/external-auth-distributed-race branch from 4c9fe79 to 5a1de17 Compare August 19, 2026 20:32

@jeremyruppel jeremyruppel left a comment

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.

nice! 👍

@code-asher
code-asher merged commit 7ba2e6e into main Aug 19, 2026
28 checks passed
@code-asher
code-asher deleted the asher/external-auth-distributed-race branch August 19, 2026 21:00
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 19, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants