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