diff --git a/coderd/gitauth/config.go b/coderd/gitauth/config.go index 43d6263a5e447..c6c058e90bc5f 100644 --- a/coderd/gitauth/config.go +++ b/coderd/gitauth/config.go @@ -22,6 +22,12 @@ type Config struct { Regex *regexp.Regexp // Type is the type of provider. Type codersdk.GitProvider + // NoRefresh stops Coder from using the refresh token + // to renew the access token. + // + // Some organizations have security policies that require + // re-authentication for every token. + NoRefresh bool } // ConvertConfig converts the YAML configuration entry to the @@ -107,6 +113,7 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con ID: entry.ID, Regex: regex, Type: typ, + NoRefresh: entry.NoRefresh, }) } return configs, nil diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index f8d10d6ff33b8..d007e804dfeb1 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1154,6 +1154,15 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request) return } + // If the token is expired and refresh is disabled, we prompt + // the user to authenticate again. + if gitAuthConfig.NoRefresh && gitAuthLink.OAuthExpiry.Before(database.Now()) { + httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentGitAuthResponse{ + URL: redirectURL.String(), + }) + return + } + token, err := gitAuthConfig.TokenSource(ctx, &oauth2.Token{ AccessToken: gitAuthLink.OAuthAccessToken, RefreshToken: gitAuthLink.OAuthRefreshToken, diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index e89b913f1bf17..98b51e532ce47 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -17,11 +17,13 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/oauth2" "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/agent" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/gitauth" "github.com/coder/coder/codersdk" "github.com/coder/coder/provisioner/echo" @@ -884,6 +886,72 @@ func TestWorkspaceAgentsGitAuth(t *testing.T) { resp = gitAuthCallback(t, "github", client) require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) }) + + t.Run("ExpiredNoRefresh", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + GitAuthConfigs: []*gitauth.Config{{ + OAuth2Config: &oauth2Config{ + token: &oauth2.Token{ + AccessToken: "token", + RefreshToken: "something", + Expiry: database.Now().Add(-time.Hour), + }, + }, + ID: "github", + Regex: regexp.MustCompile(`github\.com`), + Type: codersdk.GitProviderGitHub, + NoRefresh: true, + }}, + }) + user := coderdtest.CreateFirstUser(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.ProvisionComplete, + ProvisionApply: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + }}, + }}, + }, + }, + }}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + agentClient := codersdk.New(client.URL) + agentClient.SetSessionToken(authToken) + + token, err := agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com/asd/asd", false) + require.NoError(t, err) + require.NotEmpty(t, token.URL) + + // In the configuration, we set our OAuth provider + // to return an expired token. Coder consumes this + // and stores it. + resp := gitAuthCallback(t, "github", client) + require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + + // Because the token is expired and `NoRefresh` is specified, + // a redirect URL should be returned again. + token, err = agentClient.WorkspaceAgentGitAuth(context.Background(), "github.com/asd/asd", false) + require.NoError(t, err) + require.NotEmpty(t, token.URL) + }) + t.Run("FullFlow", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ diff --git a/codersdk/deploymentconfig.go b/codersdk/deploymentconfig.go index cb56ba5aa88e0..d23c572a7229b 100644 --- a/codersdk/deploymentconfig.go +++ b/codersdk/deploymentconfig.go @@ -124,6 +124,7 @@ type GitAuthConfig struct { AuthURL string `json:"auth_url"` TokenURL string `json:"token_url"` Regex string `json:"regex"` + NoRefresh bool `json:"no_refresh"` Scopes []string `json:"scopes"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d92fd82213ca9..735bc4932bc6a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -362,6 +362,7 @@ export interface GitAuthConfig { readonly auth_url: string readonly token_url: string readonly regex: string + readonly no_refresh: boolean readonly scopes: string[] }