juanfont/headscale · error · HTTPError

errInvalidPKCEMethod

errInvalidPKCEMethod

Error message

internal server error

What it means

Returned by the OIDC login redirect handler when a.cfg.PKCE.Method is neither 'S256' nor 'plain' (errInvalidPKCEMethod, 500). A verifier was already generated and would be sent at token exchange, so silently skipping the challenge would degrade to no-PKCE without notice — hence the hard failure instead of a silent omission.

Source

Thrown at hscontrol/oidc.go:196

	extras := make([]oauth2.AuthCodeOption, 0, len(a.cfg.ExtraParams)+defaultOAuthOptionsCount)
	// Add PKCE verification if enabled
	if a.cfg.PKCE.Enabled {
		verifier := oauth2.GenerateVerifier()
		registrationInfo.Verifier = &verifier

		extras = append(extras, oauth2.AccessTypeOffline)

		switch a.cfg.PKCE.Method {
		case types.PKCEMethodS256:
			extras = append(extras, oauth2.S256ChallengeOption(verifier))
		case types.PKCEMethodPlain:
			// oauth2 does not have a plain challenge option, so we add it manually
			extras = append(extras, oauth2.SetAuthURLParam("code_challenge_method", "plain"), oauth2.SetAuthURLParam("code_challenge", verifier))
		default:
			// An unknown method must not silently emit no challenge: a
			// verifier was generated and is sent at token exchange, so a
			// missing challenge degrades to no-PKCE without anyone noticing.
			httpError(writer, NewHTTPError(http.StatusInternalServerError, "internal server error", fmt.Errorf("%w: %q", errInvalidPKCEMethod, a.cfg.PKCE.Method)))
			return
		}
	}

	// Add any extra parameters from configuration
	for k, v := range a.cfg.ExtraParams {
		extras = append(extras, oauth2.SetAuthURLParam(k, v))
	}

	extras = append(extras, oidc.Nonce(nonce))

	// Cache the registration info
	a.authCache.Add(state, registrationInfo)

	authURL := a.oauth2Config.AuthCodeURL(state, extras...)
	log.Debug().Caller().Msgf("redirecting to %s for authentication", authURL)

	http.Redirect(writer, req, authURL, http.StatusFound)

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Set oidc.pkce.method to exactly S256 or plain (or remove it to use the default)
  2. Check the value for stray whitespace/quotes in the YAML
  3. Consult the provider's docs: most modern providers require S256

Example fix

# before
oidc:
  pkce:
    method: s256

# after
oidc:
  pkce:
    method: S256
Defensive patterns

Strategy: validation

Validate before calling

switch cfg.PKCE.Method {
case types.PKCEMethodS256, types.PKCEMethodPlain:
default:
    return fmt.Errorf("invalid oidc.pkce.method %q: must be S256 or plain", cfg.PKCE.Method)
}

Prevention

When it happens

Trigger: Configuring oidc.pkce.method to a value outside {S256, plain} (case-sensitive), or leaving an experimental/typo value like 's256' or 'SHA256'.

Common situations: YAML config typo including wrong case or whitespace; upgrading headscale where accepted PKCE method names changed; copy-pasted config from an example using an invalid value.

Understand the failure class

Related errors


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