juanfont/headscale · error

errOIDCIssuerInvalid

errOIDCIssuerInvalid

Error message

oidc.issuer must be a valid http(s) URL: got %q

What it means

Startup OIDC configuration validation error (sentinel errOIDCIssuerInvalid): oidc.issuer could not be parsed as a URL, parsed but has a scheme other than http/https, or has an empty host. Viper hands the raw string to url.Parse; any of the three conditions rejects it. The server refuses to start.

Source

Thrown at hscontrol/types/config.go:384

	return nil
}

// validateOIDCConfig validates the OIDC settings, called when oidc.issuer is
// set. It fails fast on a setup that cannot work: an invalid PKCE method, a
// malformed issuer URL (which would otherwise surface as an opaque discovery
// error or, worse, resolve to an unintended provider), or a missing client
// id/secret.
func validateOIDCConfig() error {
	err := validatePKCEMethod(viper.GetString("oidc.pkce.method"))
	if err != nil {
		return err
	}

	issuer := viper.GetString("oidc.issuer")

	u, err := url.Parse(issuer)
	if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" {
		return fmt.Errorf("%w: got %q", errOIDCIssuerInvalid, issuer)
	}

	if viper.GetString("oidc.client_id") == "" {
		return errOIDCClientIDRequired
	}

	if viper.GetString("oidc.client_secret") == "" && viper.GetString("oidc.client_secret_path") == "" {
		return errOIDCClientSecretRequired
	}

	return nil
}

// Domain returns the hostname/domain part of the [Config.ServerURL].
// If the [Config.ServerURL] is not a valid URL, it returns the [Config.BaseDomain].
func (c *Config) Domain() string {
	u, err := url.Parse(c.ServerURL)
	if err != nil {

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Set the fully-qualified issuer including scheme: oidc.issuer: https://accounts.google.com
  2. Verify the URL with curl — it must serve the OIDC discovery document at <issuer>/.well-known/openid-configuration
  3. Also confirm oidc.client_id and client_secret(_path) are set, as validation fails on those next

Example fix

# before
oidc:
  issuer: accounts.google.com

# after
oidc:
  issuer: https://accounts.google.com
  client_id: headscale
  client_secret: ...
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight check before server start:
if u, err := url.Parse(cfg.OIDC.Issuer); err != nil ||
    (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" {
    return fmt.Errorf("oidc.issuer must be absolute http(s) URL, got %q", cfg.OIDC.Issuer)
}

Type guard

func isValidOIDCIssuer(s string) bool {
    u, err := url.Parse(s)
    return err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != ""
}

Prevention

When it happens

Trigger: Config contains oidc.issuer values like 'accounts.google.com' (no scheme), 'ldap://provider' (wrong scheme), or a string that fails URL parsing entirely. validateOIDCConfig runs during config load, before PKCE passes but after the pkce.method check.

Common situations: Migrating from an older config format where the scheme was optional; copying the issuer from a provider doc that shows a bare domain; trailing whitespace or template placeholders (e.g. ${OIDC_ISSUER}) left unsubstituted by env templating.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/91e1fbb3210c4717. Report an issue: GitHub.