fix(coderd): enforce required external auth on workspace create (#26314)

Required external auth (`optional = false`) was only enforced by
client-side preflight checks, so creating a workspace via the REST API
succeeded even when the owner had never authenticated, producing a
broken workspace.

`createWorkspace` now validates the workspace owner's external auth
server-side and returns 403 before any row is inserted or prebuild is
claimed. The owner (not the initiator) is checked because build-time
token injection uses their links, so this also covers admin-on-behalf-of
creates and prebuild claims. Use `optional = true` to allow
pre-provisioning for unauthenticated users.

Fixes PLAT-241.

> This PR was generated by Coder Agents on behalf of
@dylanhuff-at-coder.
This commit is contained in:
dylanhuff-at-coder
2026-06-25 15:47:35 -07:00
committed by GitHub
parent 953091c7bc
commit fde3639714
4 changed files with 335 additions and 14 deletions
+66
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"slices"
"strconv"
"strings"
"time"
"github.com/dustin/go-humanize"
@@ -595,6 +596,27 @@ func createWorkspace(
})
}
// Required external auth is otherwise only enforced by client-side preflight
// checks in the CLI and UI, so API-created workspaces must be validated here
// before any workspace row is inserted or prebuilt workspace is claimed.
templateVersionID := req.TemplateVersionID
if templateVersionID == uuid.Nil {
templateVersionID = template.ActiveVersionID
}
templateVersion, err := api.Database.GetTemplateVersionByID(ctx, templateVersionID)
if err != nil {
if httpapi.Is404Error(err) {
return codersdk.Workspace{}, httperror.ErrResourceNotFound
}
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template version.",
Detail: err.Error(),
})
}
if err := api.requireWorkspaceOwnerExternalAuth(ctx, templateVersion, owner.ID); err != nil {
return codersdk.Workspace{}, err
}
dbAutostartSchedule, err := validWorkspaceSchedule(req.AutostartSchedule)
if err != nil {
return codersdk.Workspace{}, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
@@ -892,6 +914,50 @@ func createWorkspace(
return w, nil
}
// requireWorkspaceOwnerExternalAuth returns a 403 response error when the
// workspace owner has not authenticated with every required (non-optional)
// external auth provider referenced by the template version. Token injection
// at build time uses the owner's external auth links, so the owner is the
// subject of the check even when another user initiates the build.
func (api *API) requireWorkspaceOwnerExternalAuth(ctx context.Context, templateVersion database.TemplateVersion, ownerID uuid.UUID) error {
//nolint:gocritic // System access is required to validate the workspace owner's external auth links because admins and API clients may create workspaces for other users.
providers, err := api.templateVersionExternalAuthForUser(dbauthz.AsSystemRestricted(ctx), templateVersion, ownerID)
if err != nil {
return err
}
var (
missingNames []string
validations []codersdk.ValidationError
)
for _, provider := range providers {
if provider.Optional || provider.Authenticated {
continue
}
name := provider.DisplayName
if name == "" {
name = provider.ID
}
missingNames = append(missingNames, name)
validations = append(validations, codersdk.ValidationError{
Field: "external_auth",
Detail: provider.ID,
})
}
if len(missingNames) == 0 {
return nil
}
return httperror.NewResponseError(http.StatusForbidden, codersdk.Response{
Message: "External authentication is required to create a workspace with this template.",
Detail: fmt.Sprintf(
"The workspace owner must authenticate with the following external auth providers: %s.",
strings.Join(missingNames, ", "),
),
Validations: validations,
})
}
func requestTemplate(ctx context.Context, req codersdk.CreateWorkspaceRequest, db database.Store) (database.Template, error) {
// If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it.
templateID := req.TemplateID