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.

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.

Appendix: 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)