feat: deployment flag to auto handle changed oidc providers (#26419)

An opt-out flag exists as an escape hatch

closes https://linear.app/codercom/issue/PLAT-343/automatically-reset-user-link-for-affected-users-when-idp-provider
This commit is contained in:
Steven Masley
2026-06-16 13:26:04 -07:00
committed by GitHub
parent b6fcb9a30a
commit 0e45ded0ed
9 changed files with 286 additions and 23 deletions
+82 -23
View File
@@ -65,6 +65,7 @@ import (
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/aibridged"
"github.com/coder/coder/v2/coderd/authlink"
"github.com/coder/coder/v2/coderd/autobuild"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/awsiamrds"
@@ -117,6 +118,58 @@ import (
"github.com/coder/wgtunnel/tunnelsdk"
)
// oidcAuthLinks validates and can repair any broken OIDC auth links from changes in
// OIDC providers. This function should avoid returning a fatal error as much as possible.
// If this function fails, it should just log the error and exit.
func oidcAuthLinks(ctx context.Context, logger slog.Logger, cli *http.Client, vals *codersdk.DeploymentValues, db database.Store) error {
// nolint:gocritic // Requires system privileges
ctx = dbauthz.AsSystemRestricted(ctx)
expectedIssuer, err := authlink.ResolveIssuer(ctx, cli, vals.OIDC.IssuerURL.String())
if err != nil {
// Always log if there is a failure here
logger.Error(ctx, "unable to resolve OIDC 'issuer'",
slog.F("error", err.Error()),
slog.F("url", vals.OIDC.IssuerURL.String()),
)
return nil
}
analysis, err := authlink.AnalyzeOIDCLinks(ctx, db, expectedIssuer)
if err != nil {
// Do not make this error fatal
logger.Error(ctx, "unable to analyze OIDC links, OIDC user links cannot be verified as linked to this issuer",
slog.F("error", err.Error()),
slog.F("url", vals.OIDC.IssuerURL.String()),
slog.F("issuer", expectedIssuer),
)
return nil
}
if !vals.OIDC.AutoRepairLinks.Value() {
return nil
}
// Repair any broken OIDC links
if analysis.MismatchedTotal() > 0 {
count, err := authlink.ResetMismatchedOIDCLinks(ctx, db, expectedIssuer)
if err != nil {
logger.Error(ctx, "unable to reset mismatched OIDC links",
slog.F("error", err.Error()),
slog.F("url", vals.OIDC.IssuerURL.String()),
slog.F("issuer", expectedIssuer),
)
return nil
}
logger.Info(ctx, "oidc users OIDC links reset",
slog.F("url", vals.OIDC.IssuerURL.String()),
slog.F("issuer", expectedIssuer),
slog.F("count", count),
)
}
return nil
}
func createOIDCConfig(ctx context.Context, logger slog.Logger, vals *codersdk.DeploymentValues) (*coderd.OIDCConfig, error) {
if vals.OIDC.ClientID == "" {
return nil, xerrors.Errorf("OIDC client ID must be set!")
@@ -720,29 +773,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}
}
// As OIDC clients can be confidential or public,
// we should only check for a client id being set.
// The underlying library handles the case of no
// client secrets correctly. For more details on
// client types: https://oauth.net/2/client-types/
if vals.OIDC.ClientID != "" {
if vals.OIDC.IgnoreEmailVerified {
logger.Warn(ctx, "coder will not check email_verified for OIDC logins")
}
// This OIDC config is **not** being instrumented with the
// oauth2 instrument wrapper. If we implement the missing
// oidc methods, then we can instrument it.
// Missing:
// - Userinfo
// - Verify
oc, err := createOIDCConfig(ctx, options.Logger, vals)
if err != nil {
return xerrors.Errorf("create oidc config: %w", err)
}
options.OIDCConfig = oc
}
// We'll read from this channel in the select below that tracks shutdown. If it remains
// nil, that case of the select will just never fire, but it's important not to have a
// "bare" read on this channel.
@@ -838,6 +868,35 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return xerrors.Errorf("set deployment id: %w", err)
}
// As OIDC clients can be confidential or public,
// we should only check for a client id being set.
// The underlying library handles the case of no
// client secrets correctly. For more details on
// client types: https://oauth.net/2/client-types/
if vals.OIDC.ClientID != "" {
if vals.OIDC.IgnoreEmailVerified {
logger.Warn(ctx, "coder will not check email_verified for OIDC logins")
}
// This OIDC config is **not** being instrumented with the
// oauth2 instrument wrapper. If we implement the missing
// oidc methods, then we can instrument it.
// Missing:
// - Userinfo
// - Verify
oc, err := createOIDCConfig(ctx, options.Logger, vals)
if err != nil {
return xerrors.Errorf("create oidc config: %w", err)
}
options.OIDCConfig = oc
// Repair any existing broken OIDC
err = oidcAuthLinks(ctx, logger, httpClient, vals, options.Database)
if err != nil {
return xerrors.Errorf("oidc auth links: %w", err)
}
}
extAuthEnv, err := ReadExternalAuthProvidersFromEnv(os.Environ())
if err != nil {
return xerrors.Errorf("read external auth providers from env: %w", err)