Skip to content

Commit 110b7f2

Browse files
committed
fix: always attempt external auth refresh when fetching
1 parent 8e0a153 commit 110b7f2

File tree

1 file changed

+84
-70
lines changed

1 file changed

+84
-70
lines changed

coderd/workspaceagents.go

Lines changed: 84 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,78 +2030,25 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
20302030
return
20312031
}
20322032

2033-
if listen {
2034-
// Since we're ticking frequently and this sign-in operation is rare,
2035-
// we are OK with polling to avoid the complexity of pubsub.
2036-
ticker, done := api.NewTicker(time.Second)
2037-
defer done()
2038-
var previousToken database.ExternalAuthLink
2039-
for {
2040-
select {
2041-
case <-ctx.Done():
2042-
return
2043-
case <-ticker:
2044-
}
2045-
externalAuthLink, err := api.Database.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
2046-
ProviderID: externalAuthConfig.ID,
2047-
UserID: workspace.OwnerID,
2048-
})
2049-
if err != nil {
2050-
if errors.Is(err, sql.ErrNoRows) {
2051-
continue
2052-
}
2053-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2054-
Message: "Failed to get external auth link.",
2055-
Detail: err.Error(),
2056-
})
2057-
return
2058-
}
2059-
2060-
// Expiry may be unset if the application doesn't configure tokens
2061-
// to expire.
2062-
// See
2063-
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app.
2064-
if externalAuthLink.OAuthExpiry.Before(dbtime.Now()) && !externalAuthLink.OAuthExpiry.IsZero() {
2065-
continue
2066-
}
2067-
2068-
// Only attempt to revalidate an oauth token if it has actually changed.
2069-
// No point in trying to validate the same token over and over again.
2070-
if previousToken.OAuthAccessToken == externalAuthLink.OAuthAccessToken &&
2071-
previousToken.OAuthRefreshToken == externalAuthLink.OAuthRefreshToken &&
2072-
previousToken.OAuthExpiry == externalAuthLink.OAuthExpiry {
2073-
continue
2074-
}
2075-
2076-
valid, _, err := externalAuthConfig.ValidateToken(ctx, externalAuthLink.OAuthAccessToken)
2077-
if err != nil {
2078-
api.Logger.Warn(ctx, "failed to validate external auth token",
2079-
slog.F("workspace_owner_id", workspace.OwnerID.String()),
2080-
slog.F("validate_url", externalAuthConfig.ValidateURL),
2081-
slog.Error(err),
2082-
)
2083-
}
2084-
previousToken = externalAuthLink
2085-
if !valid {
2086-
continue
2087-
}
2088-
resp, err := createExternalAuthResponse(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken, externalAuthLink.OAuthExtra)
2089-
if err != nil {
2090-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2091-
Message: "Failed to create external auth response.",
2092-
Detail: err.Error(),
2093-
})
2094-
return
2095-
}
2096-
httpapi.Write(ctx, rw, http.StatusOK, resp)
2033+
// handleRetrying will attempt to continually check for a new token
2034+
// if listen is true. This is useful if an error is encountered in the
2035+
// original single flow.
2036+
//
2037+
// By default, if no errors are encountered, then the single flow response
2038+
// is returned.
2039+
handleRetrying := func(code int, response any) {
2040+
if !listen {
2041+
httpapi.Write(ctx, rw, code, response)
20972042
return
20982043
}
2044+
2045+
api.workspaceAgentsExternalAuthListen(rw, ctx, externalAuthConfig, workspace)
20992046
}
21002047

21012048
// This is the URL that will redirect the user with a state token.
21022049
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/external-auth/%s", externalAuthConfig.ID))
21032050
if err != nil {
2104-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2051+
handleRetrying(http.StatusInternalServerError, codersdk.Response{
21052052
Message: "Failed to parse access URL.",
21062053
Detail: err.Error(),
21072054
})
@@ -2114,36 +2061,36 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
21142061
})
21152062
if err != nil {
21162063
if !errors.Is(err, sql.ErrNoRows) {
2117-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2064+
handleRetrying(http.StatusInternalServerError, codersdk.Response{
21182065
Message: "Failed to get external auth link.",
21192066
Detail: err.Error(),
21202067
})
21212068
return
21222069
}
21232070

2124-
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.ExternalAuthResponse{
2071+
handleRetrying(http.StatusOK, agentsdk.ExternalAuthResponse{
21252072
URL: redirectURL.String(),
21262073
})
21272074
return
21282075
}
21292076

21302077
externalAuthLink, updated, err := externalAuthConfig.RefreshToken(ctx, api.Database, externalAuthLink)
21312078
if err != nil {
2132-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2079+
handleRetrying(http.StatusInternalServerError, codersdk.Response{
21332080
Message: "Failed to refresh external auth token.",
21342081
Detail: err.Error(),
21352082
})
21362083
return
21372084
}
21382085
if !updated {
2139-
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.ExternalAuthResponse{
2086+
handleRetrying(http.StatusOK, agentsdk.ExternalAuthResponse{
21402087
URL: redirectURL.String(),
21412088
})
21422089
return
21432090
}
21442091
resp, err := createExternalAuthResponse(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken, externalAuthLink.OAuthExtra)
21452092
if err != nil {
2146-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2093+
handleRetrying(http.StatusInternalServerError, codersdk.Response{
21472094
Message: "Failed to create external auth response.",
21482095
Detail: err.Error(),
21492096
})
@@ -2152,6 +2099,73 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
21522099
httpapi.Write(ctx, rw, http.StatusOK, resp)
21532100
}
21542101

2102+
func (api *API) workspaceAgentsExternalAuthListen(rw http.ResponseWriter, ctx context.Context, externalAuthConfig *externalauth.Config, workspace database.Workspace) {
2103+
// Since we're ticking frequently and this sign-in operation is rare,
2104+
// we are OK with polling to avoid the complexity of pubsub.
2105+
ticker, done := api.NewTicker(time.Second)
2106+
defer done()
2107+
var previousToken database.ExternalAuthLink
2108+
for {
2109+
select {
2110+
case <-ctx.Done():
2111+
return
2112+
case <-ticker:
2113+
}
2114+
externalAuthLink, err := api.Database.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
2115+
ProviderID: externalAuthConfig.ID,
2116+
UserID: workspace.OwnerID,
2117+
})
2118+
if err != nil {
2119+
if errors.Is(err, sql.ErrNoRows) {
2120+
continue
2121+
}
2122+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2123+
Message: "Failed to get external auth link.",
2124+
Detail: err.Error(),
2125+
})
2126+
return
2127+
}
2128+
2129+
// Expiry may be unset if the application doesn't configure tokens
2130+
// to expire.
2131+
// See
2132+
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app.
2133+
if externalAuthLink.OAuthExpiry.Before(dbtime.Now()) && !externalAuthLink.OAuthExpiry.IsZero() {
2134+
continue
2135+
}
2136+
2137+
// Only attempt to revalidate an oauth token if it has actually changed.
2138+
// No point in trying to validate the same token over and over again.
2139+
if previousToken.OAuthAccessToken == externalAuthLink.OAuthAccessToken &&
2140+
previousToken.OAuthRefreshToken == externalAuthLink.OAuthRefreshToken &&
2141+
previousToken.OAuthExpiry == externalAuthLink.OAuthExpiry {
2142+
continue
2143+
}
2144+
2145+
valid, _, err := externalAuthConfig.ValidateToken(ctx, externalAuthLink.OAuthAccessToken)
2146+
if err != nil {
2147+
api.Logger.Warn(ctx, "failed to validate external auth token",
2148+
slog.F("workspace_owner_id", workspace.OwnerID.String()),
2149+
slog.F("validate_url", externalAuthConfig.ValidateURL),
2150+
slog.Error(err),
2151+
)
2152+
}
2153+
previousToken = externalAuthLink
2154+
if !valid {
2155+
continue
2156+
}
2157+
resp, err := createExternalAuthResponse(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken, externalAuthLink.OAuthExtra)
2158+
if err != nil {
2159+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2160+
Message: "Failed to create external auth response.",
2161+
Detail: err.Error(),
2162+
})
2163+
return
2164+
}
2165+
httpapi.Write(ctx, rw, http.StatusOK, resp)
2166+
}
2167+
}
2168+
21552169
// createExternalAuthResponse creates an ExternalAuthResponse based on the
21562170
// provider type. This is to support legacy `/workspaceagents/me/gitauth`
21572171
// which uses `Username` and `Password`.

0 commit comments

Comments
 (0)