@@ -2031,26 +2031,78 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
2031
2031
return
2032
2032
}
2033
2033
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 )
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 )
2044
2098
return
2045
2099
}
2046
-
2047
- api .workspaceAgentsExternalAuthListen (ctx , rw , previousToken , externalAuthConfig , workspace )
2048
2100
}
2049
2101
2050
2102
// This is the URL that will redirect the user with a state token.
2051
2103
redirectURL , err := api .AccessURL .Parse (fmt .Sprintf ("/external-auth/%s" , externalAuthConfig .ID ))
2052
2104
if err != nil {
2053
- handleRetrying ( http .StatusInternalServerError , codersdk.Response {
2105
+ httpapi . Write ( ctx , rw , http .StatusInternalServerError , codersdk.Response {
2054
2106
Message : "Failed to parse access URL." ,
2055
2107
Detail : err .Error (),
2056
2108
})
@@ -2063,40 +2115,36 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
2063
2115
})
2064
2116
if err != nil {
2065
2117
if ! errors .Is (err , sql .ErrNoRows ) {
2066
- handleRetrying ( http .StatusInternalServerError , codersdk.Response {
2118
+ httpapi . Write ( ctx , rw , http .StatusInternalServerError , codersdk.Response {
2067
2119
Message : "Failed to get external auth link." ,
2068
2120
Detail : err .Error (),
2069
2121
})
2070
2122
return
2071
2123
}
2072
2124
2073
- handleRetrying ( http .StatusOK , agentsdk.ExternalAuthResponse {
2125
+ httpapi . Write ( ctx , rw , http .StatusOK , agentsdk.ExternalAuthResponse {
2074
2126
URL : redirectURL .String (),
2075
2127
})
2076
2128
return
2077
2129
}
2078
2130
2079
- externalAuthLink , valid , err := externalAuthConfig .RefreshToken (ctx , api .Database , externalAuthLink )
2131
+ externalAuthLink , updated , err := externalAuthConfig .RefreshToken (ctx , api .Database , externalAuthLink )
2080
2132
if err != nil {
2081
- handleRetrying ( http .StatusInternalServerError , codersdk.Response {
2133
+ httpapi . Write ( ctx , rw , http .StatusInternalServerError , codersdk.Response {
2082
2134
Message : "Failed to refresh external auth token." ,
2083
2135
Detail : err .Error (),
2084
2136
})
2085
2137
return
2086
2138
}
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 {
2139
+ if ! updated {
2140
+ httpapi .Write (ctx , rw , http .StatusOK , agentsdk.ExternalAuthResponse {
2093
2141
URL : redirectURL .String (),
2094
2142
})
2095
2143
return
2096
2144
}
2097
2145
resp , err := createExternalAuthResponse (externalAuthConfig .Type , externalAuthLink .OAuthAccessToken , externalAuthLink .OAuthExtra )
2098
2146
if err != nil {
2099
- handleRetrying ( http .StatusInternalServerError , codersdk.Response {
2147
+ httpapi . Write ( ctx , rw , http .StatusInternalServerError , codersdk.Response {
2100
2148
Message : "Failed to create external auth response." ,
2101
2149
Detail : err .Error (),
2102
2150
})
@@ -2105,81 +2153,6 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
2105
2153
httpapi .Write (ctx , rw , http .StatusOK , resp )
2106
2154
}
2107
2155
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
-
2183
2156
// createExternalAuthResponse creates an ExternalAuthResponse based on the
2184
2157
// provider type. This is to support legacy `/workspaceagents/me/gitauth`
2185
2158
// which uses `Username` and `Password`.
0 commit comments