🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

fix: prevent concurrent token refreshes - #26530

Merged
code-asher merged 9 commits into
mainfrom
asher/concurrent-token-refresh
Jul 15, 2026
Merged

fix: prevent concurrent token refreshes#26530
code-asher merged 9 commits into
mainfrom
asher/concurrent-token-refresh

Conversation

@code-asher

@code-asher code-asher commented Jun 18, 2026

Copy link
Copy Markdown
Member

This can cause bad refresh token errors, since it can only be used once.

Looks like there was an attempt to fix this by checking the database after a failed refresh, but of course this depends on the first request having updated the database in time...

@Emyrk does my fix look like it makes sense? One thing I was not sure about is if there can be multiple replicas writing in which case I actually will need a database-level lock...

Closes https://linear.app/codercom/issue/DEVEX-498/fix-external-oauth-refresh-race

and the original error report is https://linear.app/codercom/issue/DEVEX-432/github-oauth-refresh-unreliability

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

linear-code Bot commented Jun 18, 2026

Copy link
Copy Markdown

DEVEX-498

@datadog-coder

This comment has been minimized.

@code-asher
code-asher force-pushed the asher/concurrent-token-refresh branch from 71bc56f to 551304b Compare June 18, 2026 20:02

@Emyrk Emyrk 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.

Would this package make things easier?
https://pkg.go.dev/golang.org/x/sync/singleflight

As for the replicas. That remains open, but imo we can do the easy solve for a single instance, then work on the replica fix.

See #25301 for some more context on another attempt

Comment thread coderd/externalauth/externalauth.go Outdated
@code-asher
code-asher force-pushed the asher/concurrent-token-refresh branch 6 times, most recently from 4f68ded to 2d9539e Compare June 26, 2026 22:40
Comment thread coderd/util/singleflight/singleflight.go Outdated
Comment thread coderd/promoauth/oauth2.go
Comment thread coderd/promoauth/oauth2.go Outdated
@code-asher
code-asher requested a review from Emyrk June 26, 2026 22:58
@github-actions github-actions Bot added the stale This issue is like stale bread. label Jul 4, 2026
@github-actions github-actions Bot closed this Jul 7, 2026
@code-asher code-asher reopened this Jul 14, 2026
This can cause bad refresh token errors, since it can only be used once.

Only covers a single instance.
@code-asher
code-asher force-pushed the asher/concurrent-token-refresh branch from 2d9539e to 36997a7 Compare July 14, 2026 15:54

@Emyrk Emyrk 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.

Right approach for DEVEX-498; blocking issues in the custom singleflight and key choice, details inline.

Replica question: yes, replicas can still race, singleflight is per-process. But the existing re-read fallback and OldOauthRefreshToken optimistic lock bound the damage to a transient one-off error. I'd ship per-process dedup, keep the re-read fallback tested, and revisit only if error rates stay material. No database-level lock needed now; avoid SELECT FOR UPDATE on the link row, it would serialize unrelated reads and hold a lock across an external HTTP call.


This review was drafted with the assistance of Coder Agents on behalf of @Emyrk.

Comment thread coderd/util/singleflight/singleflight.go Outdated
Comment thread coderd/util/singleflight/singleflight.go Outdated
Comment thread coderd/externalauth/externalauth.go Outdated
Comment thread coderd/externalauth/externalauth.go Outdated
Comment thread coderd/promoauth/oauth2.go Outdated
Comment thread coderd/externalauth/externalauth_test.go Outdated
Comment thread coderd/externalauth/externalauth_test.go Outdated
@code-asher
code-asher force-pushed the asher/concurrent-token-refresh branch 2 times, most recently from ec2ace1 to cd69fb3 Compare July 14, 2026 19:21
@code-asher
code-asher force-pushed the asher/concurrent-token-refresh branch from cd69fb3 to 785b088 Compare July 14, 2026 20:38
@code-asher
code-asher requested a review from Emyrk July 14, 2026 20:55
@github-actions github-actions Bot removed the stale This issue is like stale bread. label Jul 15, 2026
Comment thread coderd/externalauth/externalauth.go Outdated
// Use a detached context so if a request is canceled it does not cancel all
// the other requests as well. Preserve any original deadline.
rctx := context.WithoutCancel(ctx)
deadline, ok := ctx.Deadline()

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.

Should we specify a specific deadline here? Like 10s? What is the deadline?

@code-asher code-asher Jul 15, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah I was not completely sure about this. I felt like if someone calls RefreshToken with a deadline (or not), then hopefully there was a sensible decision behind that (big if lol) and we should respect that. In tests for example I wanted to respect the test timeout.

I was thinking about a static deadline here but I was concerned that I would choose the wrong number; I am not sure what the upper bound is for completing the full refresh. Currently the refresh is bounded by 10 seconds, then another 10 seconds for updating the link, and the validation and the following update both have no separate deadline. Maybe 30s is reasonable? That seems like a LONG time to wait for a request though.

Edit to add that I think requests are currently not setting a deadline, at least the ones I have seen so far, so in production there would be no deadline. we have no reasonable way to guarantee a deadline would never be set or make sense for refreshing the token

@code-asher code-asher Jul 15, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I went with retry timeout + 10 seconds.

I also removed the separate detached context for the database update, no need since it is already detached from the request context.

@code-asher
code-asher force-pushed the asher/concurrent-token-refresh branch from 7e55096 to c1395ed Compare July 15, 2026 18:41
@code-asher
code-asher force-pushed the asher/concurrent-token-refresh branch from c1395ed to b484a70 Compare July 15, 2026 18:43
@code-asher
code-asher merged commit 4d4cbd0 into main Jul 15, 2026
28 of 29 checks passed
@code-asher
code-asher deleted the asher/concurrent-token-refresh branch July 15, 2026 20:16
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 15, 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.

2 participants