@@ -2037,78 +2037,26 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
2037
2037
return
2038
2038
}
2039
2039
2040
- if listen {
2041
- // Since we're ticking frequently and this sign-in operation is rare,
2042
- // we are OK with polling to avoid the complexity of pubsub.
2043
- ticker , done := api .NewTicker (time .Second )
2044
- defer done ()
2045
- var previousToken database.ExternalAuthLink
2046
- for {
2047
- select {
2048
- case <- ctx .Done ():
2049
- return
2050
- case <- ticker :
2051
- }
2052
- externalAuthLink , err := api .Database .GetExternalAuthLink (ctx , database.GetExternalAuthLinkParams {
2053
- ProviderID : externalAuthConfig .ID ,
2054
- UserID : workspace .OwnerID ,
2055
- })
2056
- if err != nil {
2057
- if errors .Is (err , sql .ErrNoRows ) {
2058
- continue
2059
- }
2060
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
2061
- Message : "Failed to get external auth link." ,
2062
- Detail : err .Error (),
2063
- })
2064
- return
2065
- }
2066
-
2067
- // Expiry may be unset if the application doesn't configure tokens
2068
- // to expire.
2069
- // See
2070
- // https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app.
2071
- if externalAuthLink .OAuthExpiry .Before (dbtime .Now ()) && ! externalAuthLink .OAuthExpiry .IsZero () {
2072
- continue
2073
- }
2074
-
2075
- // Only attempt to revalidate an oauth token if it has actually changed.
2076
- // No point in trying to validate the same token over and over again.
2077
- if previousToken .OAuthAccessToken == externalAuthLink .OAuthAccessToken &&
2078
- previousToken .OAuthRefreshToken == externalAuthLink .OAuthRefreshToken &&
2079
- previousToken .OAuthExpiry == externalAuthLink .OAuthExpiry {
2080
- continue
2081
- }
2082
-
2083
- valid , _ , err := externalAuthConfig .ValidateToken (ctx , externalAuthLink .OAuthAccessToken )
2084
- if err != nil {
2085
- api .Logger .Warn (ctx , "failed to validate external auth token" ,
2086
- slog .F ("workspace_owner_id" , workspace .OwnerID .String ()),
2087
- slog .F ("validate_url" , externalAuthConfig .ValidateURL ),
2088
- slog .Error (err ),
2089
- )
2090
- }
2091
- previousToken = externalAuthLink
2092
- if ! valid {
2093
- continue
2094
- }
2095
- resp , err := createExternalAuthResponse (externalAuthConfig .Type , externalAuthLink .OAuthAccessToken , externalAuthLink .OAuthExtra )
2096
- if err != nil {
2097
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
2098
- Message : "Failed to create external auth response." ,
2099
- Detail : err .Error (),
2100
- })
2101
- return
2102
- }
2103
- httpapi .Write (ctx , rw , http .StatusOK , resp )
2040
+ var previousToken * database.ExternalAuthLink
2041
+ // handleRetrying will attempt to continually check for a new token
2042
+ // if listen is true. This is useful if an error is encountered in the
2043
+ // original single flow.
2044
+ //
2045
+ // By default, if no errors are encountered, then the single flow response
2046
+ // is returned.
2047
+ handleRetrying := func (code int , response any ) {
2048
+ if ! listen {
2049
+ httpapi .Write (ctx , rw , code , response )
2104
2050
return
2105
2051
}
2052
+
2053
+ api .workspaceAgentsExternalAuthListen (ctx , rw , previousToken , externalAuthConfig , workspace )
2106
2054
}
2107
2055
2108
2056
// This is the URL that will redirect the user with a state token.
2109
2057
redirectURL , err := api .AccessURL .Parse (fmt .Sprintf ("/external-auth/%s" , externalAuthConfig .ID ))
2110
2058
if err != nil {
2111
- httpapi . Write ( ctx , rw , http .StatusInternalServerError , codersdk.Response {
2059
+ handleRetrying ( http .StatusInternalServerError , codersdk.Response {
2112
2060
Message : "Failed to parse access URL." ,
2113
2061
Detail : err .Error (),
2114
2062
})
@@ -2121,36 +2069,40 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
2121
2069
})
2122
2070
if err != nil {
2123
2071
if ! errors .Is (err , sql .ErrNoRows ) {
2124
- httpapi . Write ( ctx , rw , http .StatusInternalServerError , codersdk.Response {
2072
+ handleRetrying ( http .StatusInternalServerError , codersdk.Response {
2125
2073
Message : "Failed to get external auth link." ,
2126
2074
Detail : err .Error (),
2127
2075
})
2128
2076
return
2129
2077
}
2130
2078
2131
- httpapi . Write ( ctx , rw , http .StatusOK , agentsdk.ExternalAuthResponse {
2079
+ handleRetrying ( http .StatusOK , agentsdk.ExternalAuthResponse {
2132
2080
URL : redirectURL .String (),
2133
2081
})
2134
2082
return
2135
2083
}
2136
2084
2137
- externalAuthLink , updated , err := externalAuthConfig .RefreshToken (ctx , api .Database , externalAuthLink )
2085
+ externalAuthLink , valid , err := externalAuthConfig .RefreshToken (ctx , api .Database , externalAuthLink )
2138
2086
if err != nil {
2139
- httpapi . Write ( ctx , rw , http .StatusInternalServerError , codersdk.Response {
2087
+ handleRetrying ( http .StatusInternalServerError , codersdk.Response {
2140
2088
Message : "Failed to refresh external auth token." ,
2141
2089
Detail : err .Error (),
2142
2090
})
2143
2091
return
2144
2092
}
2145
- if ! updated {
2146
- httpapi .Write (ctx , rw , http .StatusOK , agentsdk.ExternalAuthResponse {
2093
+ if ! valid {
2094
+ // Set the previous token so the retry logic will skip validating the
2095
+ // same token again. This should only be set if the token is invalid and there
2096
+ // was no error. If it is invalid because of an error, then we should recheck.
2097
+ previousToken = & externalAuthLink
2098
+ handleRetrying (http .StatusOK , agentsdk.ExternalAuthResponse {
2147
2099
URL : redirectURL .String (),
2148
2100
})
2149
2101
return
2150
2102
}
2151
2103
resp , err := createExternalAuthResponse (externalAuthConfig .Type , externalAuthLink .OAuthAccessToken , externalAuthLink .OAuthExtra )
2152
2104
if err != nil {
2153
- httpapi . Write ( ctx , rw , http .StatusInternalServerError , codersdk.Response {
2105
+ handleRetrying ( http .StatusInternalServerError , codersdk.Response {
2154
2106
Message : "Failed to create external auth response." ,
2155
2107
Detail : err .Error (),
2156
2108
})
@@ -2159,6 +2111,81 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
2159
2111
httpapi .Write (ctx , rw , http .StatusOK , resp )
2160
2112
}
2161
2113
2114
+ func (api * API ) workspaceAgentsExternalAuthListen (ctx context.Context , rw http.ResponseWriter , previous * database.ExternalAuthLink , externalAuthConfig * externalauth.Config , workspace database.Workspace ) {
2115
+ // Since we're ticking frequently and this sign-in operation is rare,
2116
+ // we are OK with polling to avoid the complexity of pubsub.
2117
+ ticker , done := api .NewTicker (time .Second )
2118
+ defer done ()
2119
+ // If we have a previous token that is invalid, we should not check this again.
2120
+ // This serves to prevent doing excessive unauthorized requests to the external
2121
+ // auth provider. For github, this limit is 60 per hour, so saving a call
2122
+ // per invalid token can be significant.
2123
+ var previousToken database.ExternalAuthLink
2124
+ if previous != nil {
2125
+ previousToken = * previous
2126
+ }
2127
+ for {
2128
+ select {
2129
+ case <- ctx .Done ():
2130
+ return
2131
+ case <- ticker :
2132
+ }
2133
+ externalAuthLink , err := api .Database .GetExternalAuthLink (ctx , database.GetExternalAuthLinkParams {
2134
+ ProviderID : externalAuthConfig .ID ,
2135
+ UserID : workspace .OwnerID ,
2136
+ })
2137
+ if err != nil {
2138
+ if errors .Is (err , sql .ErrNoRows ) {
2139
+ continue
2140
+ }
2141
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
2142
+ Message : "Failed to get external auth link." ,
2143
+ Detail : err .Error (),
2144
+ })
2145
+ return
2146
+ }
2147
+
2148
+ // Expiry may be unset if the application doesn't configure tokens
2149
+ // to expire.
2150
+ // See
2151
+ // https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app.
2152
+ if externalAuthLink .OAuthExpiry .Before (dbtime .Now ()) && ! externalAuthLink .OAuthExpiry .IsZero () {
2153
+ continue
2154
+ }
2155
+
2156
+ // Only attempt to revalidate an oauth token if it has actually changed.
2157
+ // No point in trying to validate the same token over and over again.
2158
+ if previousToken .OAuthAccessToken == externalAuthLink .OAuthAccessToken &&
2159
+ previousToken .OAuthRefreshToken == externalAuthLink .OAuthRefreshToken &&
2160
+ previousToken .OAuthExpiry == externalAuthLink .OAuthExpiry {
2161
+ continue
2162
+ }
2163
+
2164
+ valid , _ , err := externalAuthConfig .ValidateToken (ctx , externalAuthLink .OAuthToken ())
2165
+ if err != nil {
2166
+ api .Logger .Warn (ctx , "failed to validate external auth token" ,
2167
+ slog .F ("workspace_owner_id" , workspace .OwnerID .String ()),
2168
+ slog .F ("validate_url" , externalAuthConfig .ValidateURL ),
2169
+ slog .Error (err ),
2170
+ )
2171
+ }
2172
+ previousToken = externalAuthLink
2173
+ if ! valid {
2174
+ continue
2175
+ }
2176
+ resp , err := createExternalAuthResponse (externalAuthConfig .Type , externalAuthLink .OAuthAccessToken , externalAuthLink .OAuthExtra )
2177
+ if err != nil {
2178
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
2179
+ Message : "Failed to create external auth response." ,
2180
+ Detail : err .Error (),
2181
+ })
2182
+ return
2183
+ }
2184
+ httpapi .Write (ctx , rw , http .StatusOK , resp )
2185
+ return
2186
+ }
2187
+ }
2188
+
2162
2189
// createExternalAuthResponse creates an ExternalAuthResponse based on the
2163
2190
// provider type. This is to support legacy `/workspaceagents/me/gitauth`
2164
2191
// which uses `Username` and `Password`.
0 commit comments