siyuan-note/siyuan · error

OIDC login transaction was not found or has expired

Error message

OIDC login transaction was not found or has expired

What it means

claimOIDCTransaction looks up the pending transaction by state after cleaning up expired entries. If no transaction with that state exists — either it expired after oidcTransactionTimeout, was never created, or was already removed — the callback cannot be matched to a login and this error is returned.

Solutions

  1. Restart the OIDC login from scratch — a fresh state will be issued
  2. Complete the IdP login promptly, within oidcTransactionTimeout
  3. If running multiple instances, enable sticky sessions or a shared transaction store so start and callback hit the same node
Defensive patterns

Strategy: try-catch

Try / catch

tx, done, err := claimOIDCTransaction(ctx, state, binding, false)
if err != nil {
    // treat as expired: redirect the user to restart the OIDC login
    redirectToLoginStart(w, r)
    return
}

Prevention

When it happens

Trigger: OIDCCallback/OIDCMobileCallback arrives with a state value that is not in oidcTransactions.byState after cleanupOIDCTransactionsLocked: expired login, server restart, state from a different instance, or double-claimed then deleted transaction.

Common situations: User leaves the IdP login page open longer than the transaction timeout, then completes it; kernel restarted between login start and callback; load-balanced setup sending start and callback to different nodes.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at kernel/model/oidc.go:689

	}
	oidcTransactions.byState[transaction.State] = transaction
	if transaction.PollToken != "" {
		oidcTransactions.byPoll[transaction.PollToken] = transaction.State
	}
	return nil
}

func claimOIDCTransaction(ctx context.Context, state, binding string,
	allowDesktopWithoutBinding bool) (*oidcTransaction, bool, error) {
	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

View on GitHub (pinned to 9f775e8a12)