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