juanfont/headscale · error

authenticated principal has an unverified email

Error message

authenticated principal has an unverified email

What it means

Returned by the OIDC email verification check (hscontrol/oidc.go:586) when the authenticated principal's email has email_verified=false (or the flag is absent and force_verified_email is enabled). HTTP 401 'unverified email'. Headscale refuses to map an unverified email onto a tailnet identity.

Solutions

  1. Have the user verify their email in the IdP, then retry login
  2. If the IdP cannot provide verification, disable the force-verified-email requirement in headscale.yaml
  3. For dev IdPs, enable the 'email as verified' option (e.g. Keycloak's 'Verify email' off / Dex mock OIDC)
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: oidc.force_verified_email (or equivalent policy requiring verified email) is on and the IdP reports the user's email as not verified, or the IdP omits the email_verified claim entirely.

Common situations: Development IdPs (Keycloak/Dex dev mode) that never mark emails verified; users with pending email confirmation; IdP that does not implement email_verified at all; use_email_as_name or domain restrictions layered on top.

Understand the failure class

Related errors


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

Appendix: source

Thrown at hscontrol/oidc.go:57

	// the callback boundary so [getCookieName] cannot panic on a slice
	// out-of-range.
	cookieNamePrefixLen = 6
)

var errOIDCStateTooShort = errors.New("oidc state parameter is too short")

var (
	errEmptyOIDCCallbackParams = errors.New("empty OIDC callback params")
	errNoOIDCIDToken           = errors.New("extracting ID token")
	errNoOIDCRegistrationInfo  = errors.New("registration info not in cache")
	errOIDCAllowedDomains      = errors.New(
		"authenticated principal does not match any allowed domain",
	)
	errOIDCAllowedGroups = errors.New("authenticated principal is not in any allowed group")
	errOIDCAllowedUsers  = errors.New(
		"authenticated principal does not match any allowed user",
	)
	errOIDCUnverifiedEmail = errors.New("authenticated principal has an unverified email")
	errInvalidPKCEMethod   = errors.New("invalid pkce.method")
)

// AuthInfo contains both auth ID and verifier information for OIDC validation.
type AuthInfo struct {
	AuthID       types.AuthID
	Verifier     *string
	Registration bool
}

type AuthProviderOIDC struct {
	h         *Headscale
	serverURL string
	cfg       *types.OIDCConfig

	// authCache holds auth information between the auth and the callback
	// steps. It is a bounded [expirable.LRU] keyed by OIDC state, evicting oldest
	// entries to keep the cache footprint constant under attack.

View on GitHub (pinned to 565fd254d0)