goharbor/harbor · error

State mismatch

Error message

State mismatch

What it means

OIDC Callback answers HTTP 400 'State mismatch' when the state query parameter returned by the OIDC provider differs from the state stored in the user's Harbor session. This is the CSRF check of the authorization-code flow: the callback must land in the same browser session that initiated login.

Source

Thrown at src/core/controllers/oidc.go:113

		return
	}
	if err := oc.SetSession(stateKey, state); err != nil {
		log.Errorf("failed to set session for key: %s, error: %v", stateKey, err)
		oc.SendInternalServerError(err)
		return
	}
	log.Debugf("State dumped to session: %s", state)
	// Force to use the func 'Redirect' of beego.Controller
	oc.Controller.Redirect(url, http.StatusFound)
}

// Callback handles redirection from OIDC provider.  It will exchange the token and
// kick off onboard if needed.
func (oc *OIDCController) Callback() {
	if oc.Ctx.Request.URL.Query().Get("state") != oc.GetSession(stateKey) {
		log.Errorf("State mismatch, in session: %s, in url: %s", oc.GetSession(stateKey),
			oc.Ctx.Request.URL.Query().Get("state"))
		oc.SendBadRequestError(errors.New("State mismatch"))
		return
	}
	errorCode := oc.Ctx.Request.URL.Query().Get("error")
	if errorCode != "" {
		errorDescription := oc.Ctx.Request.URL.Query().Get("error_description")
		log.Errorf("OIDC callback returned error: %s - %s", errorCode, errorDescription)
		oc.SendBadRequestError(errors.Errorf("OIDC callback returned error: %s - %s", errorCode, errorDescription))
		return
	}
	var redirectURLStr string
	redirectURL := oc.GetSession(redirectURLKey)
	if redirectURL != nil {
		redirectURLStr = redirectURL.(string)
		if err := oc.DelSession(redirectURLKey); err != nil {
			log.Errorf("failed to delete session for key:%s, error: %v", redirectURLKey, err)
			oc.SendInternalServerError(err)
			return
		}

View on GitHub (pinned to 7b2fd08cc5)

Solutions

  1. Restart the login from the Harbor sign-on page and finish it in the same browser
  2. Enable session affinity (or a shared session store) between core replicas
  3. Check the browser accepts Harbor's session cookie (domain, Secure, SameSite)
  4. Verify the OIDC redirect URI registered at the provider exactly matches Harbor's callback URL
Defensive patterns

Strategy: retry

Validate before calling

// Client-side: verify the browser still holds the Harbor session before redirecting to the IdP
if !harborSessionCookiePresent(document) { /* clear login state and start fresh */ }

Type guard

func isStateMismatch(resp *http.Response) bool {
    return resp.StatusCode == http.StatusBadRequest && strings.Contains(readBody(resp), "State mismatch")
}

Try / catch

// OIDC client middleware: on state mismatch, restart the flow once
if isStateMismatch(resp) {
    resp = restartLoginFlow() // new state, new nonce, same session cookie
}

Prevention

When it happens

Trigger: GET /c/oidc/callback when the Harbor session cookie was not sent (blocked, incognito, new browser), the flow was started in a different browser, an old callback URL is replayed, or the session lives in another core replica.

Common situations: Core replicas behind a load balancer without sticky sessions (session affinity); SameSite/Secure cookie settings blocking the session cookie; clock- or expiry-related session loss; user copying the callback URL into another tab.

Related errors


AI-assisted analysis of goharbor/harbor@7b2fd08cc5 (2026-08-16). Data as JSON: /api/errors/d28ca863c9b4b4d7. Report an issue: GitHub.