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