siyuan-note/siyuan · error

OIDC login binding does not match

Error message

OIDC login binding does not match

What it means

Transactions are bound to a per-client binding value (desktop window binding / mobile flow). claimOIDCTransaction requires the binding supplied at claim time to equal the one stored at creation, unless the desktop/validate flow explicitly allows claiming without a binding. On mismatch (or a missing binding when required) it fails with this error, preventing one browser context from hijacking another's login.

Solutions

  1. Complete the login in the same browser/window that initiated it
  2. Start a new OIDC login from the client that should receive the session
  3. Check that cookies/local storage holding the binding are not being cleared or blocked (third-party cookie settings, private mode)
Defensive patterns

Strategy: validation

Validate before calling

// client side: refuse to open the callback without the stored binding
if binding == "" {
    return errors.New("missing OIDC binding; restart login from the original window")
}

Prevention

When it happens

Trigger: OIDCCallback/OIDCMobileCallback presents a binding that differs from the transaction's Binding, or an empty binding for a flow that requires one (non-desktop/validate flows).

Common situations: Completing the login in a different browser or device than the one that started it; cookie/session lost so the binding is empty; a desktop window restarted between start and callback; proxy altering cookies.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/9d5e8d1d60a42c4d. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/oidc.go:699

	if state == "" {
		return nil, false, errors.New("OIDC state is missing")
	}
	oidcTransactions.Lock()
	cleanupOIDCTransactionsLocked()
	transaction := oidcTransactions.byState[state]
	if transaction == nil {
		oidcTransactions.Unlock()
		return nil, false, errors.New("OIDC login transaction was not found or has expired")
	}
	if transaction.ConfigVersion != oidcConfigurationVersion(Conf.GetOIDC()) {
		deleteOIDCTransactionLocked(state)
		oidcTransactions.Unlock()
		return nil, false, errors.New("OIDC configuration changed during login")
	}
	if !(allowDesktopWithoutBinding && (transaction.Flow == oidcFlowDesktop || transaction.Flow == oidcFlowValidate)) &&
		(binding == "" || binding != transaction.Binding) {
		oidcTransactions.Unlock()
		return nil, false, errors.New("OIDC login binding does not match")
	}
	if !transaction.Claimed {
		transaction.Claimed = true
		copy := *transaction
		oidcTransactions.Unlock()
		return &copy, false, nil
	}
	done := transaction.Done
	oidcTransactions.Unlock()

	select {
	case <-ctx.Done():
		return nil, false, fmt.Errorf("wait for OIDC login transaction failed: %w", ctx.Err())
	case <-done:
	}

	oidcTransactions.Lock()
	defer oidcTransactions.Unlock()

View on GitHub (pinned to 9f775e8a12)