netbirdio/netbird · error

authentication failed: Invalid state

Error message

authentication failed: Invalid state

What it means

The state query parameter on the IdP callback did not match the state generated at flow start (subtle.ConstantTimeCompare failed). The state is 24 random bytes in hex binding this browser round-trip to this flow instance; a mismatch means the callback belongs to a different flow, a stale one, or is forged. The constant-time compare prevents leaking how many bytes matched.

Source

Thrown at client/internal/auth/pkce_flow.go:269

	if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		errChan <- err
	}
}

func (p *PKCEAuthorizationFlow) handleRequest(req *http.Request) (*oauth2.Token, error) {
	query := req.URL.Query()

	if authError := query.Get(queryError); authError != "" {
		authErrorDesc := query.Get(queryErrorDesc)
		if authErrorDesc != "" {
			return nil, fmt.Errorf("authentication failed: %s", authErrorDesc)
		}
		return nil, fmt.Errorf("authentication failed: %s", authError)
	}

	// Prevent timing attacks on the state
	if state := query.Get(queryState); subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
		return nil, fmt.Errorf("authentication failed: Invalid state")
	}

	code := query.Get(queryCode)
	if code == "" {
		return nil, fmt.Errorf("authentication failed: missing code")
	}

	exchangeStart := time.Now()
	token, err := p.oAuthConfig.Exchange(
		req.Context(),
		code,
		oauth2.SetAuthURLParam("code_verifier", p.codeVerifier),
	)
	if err != nil {
		return nil, err
	}

	log.Infof("pkce flow: authorization code exchanged for token in %s", time.Since(exchangeStart).Round(time.Millisecond))

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Close all stale NetBird login tabs and retry the login to start a flow with a fresh state.
  2. Run only one login flow at a time - a second flow invalidates the first flow's state.
  3. Do not refresh or bookmark the localhost callback page; complete the flow once and let the browser land on the rendered success template.
  4. If it persists, compare the state parameter in the failing callback URL against the state in the authorization URL the client opened.
Defensive patterns

Strategy: try-catch

Try / catch

_, err := flow.WaitToken(ctx, info)
if err != nil {
    if strings.Contains(err.Error(), "Invalid state") {
        // stale or forged callback: restart the flow with RequestAuthInfo
        // instead of retrying the callback URL
    }
}

Prevention

When it happens

Trigger: A callback from a previous login attempt arrives after a newer RequestAuthInfo overwrote p.state; two login flows run concurrently on the same machine; the callback URL is reloaded (browser refresh, prefetch, or a duplicate tab) replaying the old state; a crafted request hits the local server with a wrong or missing state.

Common situations: User refreshes the 'login successful' page, re-sending the old query; user runs netbird up twice and completes the first tab after the second flow armed a new state; browser extensions prefetching the redirect URL; bookmarked or manually re-opened localhost callback URLs.

Understand the failure class

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/c5180ecac21baa32. Report an issue: GitHub.