Skip to content

Commit 3b6a39f

Browse files
committed
fix: always attempt external auth refresh when fetching
1 parent ecae6f9 commit 3b6a39f

File tree

1 file changed

+84
-70
lines changed

1 file changed

+84
-70
lines changed

coderd/workspaceagents.go

+84-70
Original file line numberDiff line numberDiff line change
@@ -2031,78 +2031,25 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
20312031
return
20322032
}
20332033

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

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

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

21312078
externalAuthLink, updated, err := externalAuthConfig.RefreshToken(ctx, api.Database, externalAuthLink)
21322079
if err != nil {
2133-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2080+
handleRetrying(http.StatusInternalServerError, codersdk.Response{
21342081
Message: "Failed to refresh external auth token.",
21352082
Detail: err.Error(),
21362083
})
21372084
return
21382085
}
21392086
if !updated {
2140-
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.ExternalAuthResponse{
2087+
handleRetrying(http.StatusOK, agentsdk.ExternalAuthResponse{
21412088
URL: redirectURL.String(),
21422089
})
21432090
return
21442091
}
21452092
resp, err := createExternalAuthResponse(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken, externalAuthLink.OAuthExtra)
21462093
if err != nil {
2147-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
2094+
handleRetrying(http.StatusInternalServerError, codersdk.Response{
21482095
Message: "Failed to create external auth response.",
21492096
Detail: err.Error(),
21502097
})
@@ -2153,6 +2100,73 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
21532100
httpapi.Write(ctx, rw, http.StatusOK, resp)
21542101
}
21552102

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

0 commit comments

Comments
 (0)